401 lines
22 KiB
TypeScript
401 lines
22 KiB
TypeScript
/**
|
||
* 自动化(Automation)通用流程 helper —— Android 实测(2026-06)。
|
||
* 自动化 = 条件(Schedules 定时)+ 动作(复用 scene 的 selectSmartDeviceAction)。
|
||
* 入口: Automations tab → Create(addBto)→ dismissGuideOverlay(首次引导)
|
||
* 条件: Add condition → Schedules → 时间轮设目标时间 → Save
|
||
* 动作: Add action → selectSmartDeviceAction(设备, 动作)
|
||
* 命名+Save → 到点触发 → 右上"..."→ Automation Logs 验证执行 → Edit Automation→Delete 删除
|
||
*
|
||
* 时间轮(1080×2280):小时列 x≈425、分钟列 x≈650,选中带中心 y≈1908,1 格=上滑 95px(慢滑,无惯性)。
|
||
*/
|
||
import { DeviceDriver } from '../../drivers/types';
|
||
import { sleep } from './element.helper';
|
||
import { dismissGuideOverlay, dismissSaveConfirmCancel } from './navigation.helper';
|
||
import { selectSmartDeviceAction } from './scene.helper';
|
||
|
||
const HOUR_X = 425;
|
||
const MIN_X = 650;
|
||
const WHEEL_TOP = 1855; // 上滑终点(中心上方一格)
|
||
const WHEEL_MID = 1950; // 上滑起点
|
||
|
||
async function tapText(driver: DeviceDriver, t: string): Promise<boolean> {
|
||
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`);
|
||
if (el) { await driver.tapElement(el); return true; }
|
||
return false;
|
||
}
|
||
async function tapContains(driver: DeviceDriver, t: string): Promise<boolean> {
|
||
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${t}")`);
|
||
if (el) { await driver.tapElement(el); return true; }
|
||
return false;
|
||
}
|
||
async function tapId(driver: DeviceDriver, id: string): Promise<boolean> {
|
||
const el = await driver.findElementRaw('id', `com.theswitchbot.switchbot:id/${id}`);
|
||
if (el) { await driver.tapElement(el); return true; }
|
||
return false;
|
||
}
|
||
|
||
/** 在某一列(x)上滑 n 格(n>0 增大/上滑;n<0 减小/下滑)。1 格 95px。 */
|
||
async function spinWheel(driver: DeviceDriver, x: number, n: number): Promise<void> {
|
||
const up = n > 0;
|
||
for (let i = 0; i < Math.abs(n); i++) {
|
||
if (up) await driver.swipe(x, WHEEL_MID, x, WHEEL_TOP, 0.5);
|
||
else await driver.swipe(x, WHEEL_TOP, x, WHEEL_MID, 0.5);
|
||
await sleep(250);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设定 Schedules 时间轮到 targetH:targetM。先读 "Time HH:MM >" 行当前值作起点(新自动化默认 08:00),
|
||
* 打开时间轮 → 小时/分钟列各按差值就近方向滑动 → OK。
|
||
*/
|
||
export async function setScheduleTime(driver: DeviceDriver, targetH: number, targetM: number): Promise<void> {
|
||
const src = await driver.getSource();
|
||
const m = src.match(/text="(\d{2}):(\d{2})"/);
|
||
const startH = m ? parseInt(m[1], 10) : 8;
|
||
const startM = m ? parseInt(m[2], 10) : 0;
|
||
|
||
// 打开时间轮:点 "Time HH:MM" 行(用当前时间值文本定位)
|
||
const timeText = `${String(startH).padStart(2, '0')}:${String(startM).padStart(2, '0')}`;
|
||
if (!(await tapText(driver, timeText))) await tapContains(driver, 'Time');
|
||
await sleep(1500);
|
||
|
||
// 就近方向步数(小时 mod 24,分钟 mod 60)
|
||
const upH = ((targetH - startH) % 24 + 24) % 24;
|
||
const hSteps = upH <= 12 ? upH : -(24 - upH);
|
||
const upM = ((targetM - startM) % 60 + 60) % 60;
|
||
const mSteps = upM <= 30 ? upM : -(60 - upM);
|
||
|
||
await spinWheel(driver, HOUR_X, hSteps);
|
||
await spinWheel(driver, MIN_X, mSteps);
|
||
await sleep(500);
|
||
await tapText(driver, 'OK');
|
||
await sleep(1500);
|
||
}
|
||
|
||
export interface AutomationConfig {
|
||
name: string;
|
||
deviceKeyword: string; // 动作设备(如 'Bot 14' / 'Curtain 89')
|
||
targetHour: number; // 定时小时(由调用方按设备当前时间+偏移算出)
|
||
targetMinute: number;
|
||
action?: string; // 优先动作文本(Turns off / Presses once 等)
|
||
}
|
||
|
||
// ============ iOS 适配(实测 2026-06-23,iPhone 17.5.1,需系统 24 小时制) ============
|
||
// 入口:底部 Automations tab → Create 按钮 → (首次 coach-mark)→ Add condition → Schedules → Time 行 → 时间轮(24h 2轮)→ OK → Save
|
||
// → Add action → Smart Devices → 滚动找设备 → 动作 → 命名 → Save。时间轮按 setPickerWheelValue 整串设值。
|
||
const pad2 = (n: number) => String(n).padStart(2, '0');
|
||
|
||
/** iOS:点 name 元素(button,element click 不受键盘遮挡)。 */
|
||
async function tapNameIOS(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
const el = await driver.findElementRaw('name', name).catch(() => null)
|
||
|| await driver.findElementRaw('predicate string', `name == "${name}" OR label == "${name}"`).catch(() => null);
|
||
if (el) { await driver.clickElement(el).catch(async () => { await driver.tapElement(el); }); return true; }
|
||
return false;
|
||
}
|
||
|
||
/** iOS:点 "Add condition"/"Add action" 这类行(同名有大容器+窄行两个,选窄的 width<360 点中心)。 */
|
||
async function tapNarrowRowIOS(driver: DeviceDriver, prefix: string): Promise<boolean> {
|
||
const els = await driver.findElementsRaw('predicate string', `name BEGINSWITH "${prefix}"`).catch(() => [] as string[]);
|
||
for (const el of els) {
|
||
const r = await driver.getElementRect(el).catch(() => null);
|
||
if (r && r.width > 0 && r.width < 360) { await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); return true; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** iOS:关首次进 Create Automation 的 coach-mark(Next/Got it 深色按钮居中、accessibility 无名字 → 按坐标点;仅有引导文字时点)。 */
|
||
async function dismissAutomationCoachIOS(driver: DeviceDriver, W: number, H: number): Promise<void> {
|
||
const MARK = ['trigger this Automation', 'Add what will', 'set dates to customize', 'will trigger', 'customize Automations'];
|
||
for (let i = 0; i < 4; i++) {
|
||
const src = await driver.getSource();
|
||
if (!MARK.some((m) => src.includes(m))) break;
|
||
await driver.tap(Math.round(W * 0.5), Math.round(H * 0.58)); // 深色 Next/Got it 按钮(实测 844 高时 ≈y489)
|
||
await sleep(1200);
|
||
}
|
||
}
|
||
|
||
/** iOS:设 Schedules 时间轮(系统 24h → 2 轮:时 00-23 / 分 00-59)。整串设值,设后轮子会 stale 需重取。 */
|
||
async function setScheduleTimeIOS(driver: DeviceDriver, targetH: number, targetM: number): Promise<void> {
|
||
const getWheels = () => driver.findElementsRaw('class name', 'XCUIElementTypePickerWheel').catch(() => [] as string[]);
|
||
const setVal = (id: string, v: string) => (driver as unknown as { setPickerWheelValue(id: string, v: string): Promise<void> }).setPickerWheelValue(id, v).catch(() => {});
|
||
let w = await getWheels();
|
||
if (w.length >= 2) { await setVal(w[0], pad2(targetH)); await sleep(1000); }
|
||
w = await getWheels(); // 设完 stale,重取
|
||
if (w.length >= 2) { await setVal(w[1], pad2(targetM)); await sleep(1000); }
|
||
}
|
||
|
||
/** iOS:动作源页 → Smart Devices → 滚动找设备(首页滑动法+可视区)→ 选动作(actionPref / Turns off|on / Presses once)。 */
|
||
async function selectSmartDeviceActionIOS(driver: DeviceDriver, deviceKeyword: string, actionPref?: string): Promise<boolean> {
|
||
const sd = await driver.findElementRaw('predicate string', 'name BEGINSWITH "Smart Devices"').catch(() => null);
|
||
if (!sd) { console.log('FAIL: no Smart Devices(iOS)'); return false; }
|
||
{ const r = await driver.getElementRect(sd); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); }
|
||
await sleep(2000);
|
||
const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 }));
|
||
let picked = false, lastSig = '';
|
||
for (let r = 0; r < 14 && !picked; r++) {
|
||
const el = await driver.findElementRaw('predicate string', `name CONTAINS "${deviceKeyword}" OR label CONTAINS "${deviceKeyword}"`).catch(() => null);
|
||
if (el) {
|
||
const rect = await driver.getElementRect(el).catch(() => null);
|
||
if (rect && rect.width > 0 && rect.y > winH * 0.11 && rect.y + rect.height < winH * 0.95) {
|
||
await driver.tap(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2)); picked = true; break;
|
||
}
|
||
}
|
||
const sig = (await driver.getSource()).slice(-2500);
|
||
if (sig === lastSig) break; lastSig = sig;
|
||
await driver.scrollDown(500); await sleep(900);
|
||
}
|
||
if (!picked) { console.log(`FAIL: no ${deviceKeyword}(iOS 动作设备)`); return false; }
|
||
await sleep(1500);
|
||
for (const a of [actionPref, 'Turns off', 'Turns on', 'Presses once'].filter(Boolean) as string[]) {
|
||
const el = await driver.findElementRaw('predicate string', `name == "${a}" OR label == "${a}"`).catch(() => null);
|
||
if (el) { const r = await driver.getElementRect(el); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); await sleep(1500); return true; }
|
||
}
|
||
console.log('FAIL: no action option(iOS)'); return false;
|
||
}
|
||
|
||
/** iOS:进 Automations tab 列表(底部 tab)。 */
|
||
async function gotoAutomationsTabIOS(driver: DeviceDriver, W: number, H: number): Promise<void> {
|
||
const tab = await driver.findElementRaw('predicate string', 'name == "Automations"').catch(() => null);
|
||
if (tab) { const r = await driver.getElementRect(tab); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); }
|
||
else { await driver.tap(Math.round(W * 0.5), H - 40); } // 兜底:底部中间 tab
|
||
await sleep(1500);
|
||
}
|
||
|
||
async function createAutomationIOS(driver: DeviceDriver, cfg: AutomationConfig): Promise<boolean> {
|
||
const { width: W, height: H } = await driver.getWindowSize().catch(() => ({ width: 390, height: 844 }));
|
||
await driver.restartApp?.().catch(() => {}); // 复位到干净首页(避免残留态)
|
||
await sleep(2500);
|
||
await driver.dismissPopupIfPresent().catch(() => {});
|
||
|
||
// 1) Automations tab → Create
|
||
await gotoAutomationsTabIOS(driver, W, H);
|
||
// 轮询等 Create 按钮渲染(页面切换+列表渲染可能 >1.5s);找到 element click,兜底坐标点。
|
||
let createOk = false;
|
||
for (let i = 0; i < 8 && !createOk; i++) {
|
||
const createBtn = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeButton" AND name == "Create"').catch(() => null);
|
||
if (createBtn) {
|
||
const r = await driver.getElementRect(createBtn).catch(() => null);
|
||
if (r && r.width > 0) { await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); createOk = true; break; }
|
||
}
|
||
if (i === 3) await gotoAutomationsTabIOS(driver, W, H); // 中途没出来,重点一次 tab
|
||
await sleep(800);
|
||
}
|
||
if (!createOk) { console.log('FAIL: no Create(iOS)'); return false; }
|
||
await sleep(2000);
|
||
await dismissAutomationCoachIOS(driver, W, H); // 首次 coach-mark
|
||
|
||
// 2) 条件:Add condition → Schedules → Time 行 → 时间轮 → OK → Save
|
||
if (!(await tapNarrowRowIOS(driver, 'Add condition'))) { console.log('FAIL: no Add condition(iOS)'); return false; }
|
||
await sleep(1500);
|
||
const sched = await driver.findElementRaw('predicate string', 'name BEGINSWITH "Schedules"').catch(() => null);
|
||
if (!sched) { console.log('FAIL: no Schedules(iOS)'); return false; }
|
||
{ const r = await driver.getElementRect(sched); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); }
|
||
await sleep(2000);
|
||
const timeRow = await driver.findElementRaw('predicate string', 'name BEGINSWITH "Time "').catch(() => null);
|
||
if (timeRow) { const r = await driver.getElementRect(timeRow); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); await sleep(2000); }
|
||
await setScheduleTimeIOS(driver, cfg.targetHour, cfg.targetMinute);
|
||
await tapNameIOS(driver, 'OK'); await sleep(1500); // 关时间模态
|
||
await tapNameIOS(driver, 'Save'); await sleep(2000); // Schedules 页 Save
|
||
|
||
// 3) 动作:Add action → Smart Devices → 设备 → 动作
|
||
if (!(await tapNarrowRowIOS(driver, 'Add action'))) { console.log('FAIL: no Add action(iOS)'); return false; }
|
||
await sleep(2000);
|
||
if (!(await selectSmartDeviceActionIOS(driver, cfg.deviceKeyword, cfg.action))) return false;
|
||
await sleep(1500);
|
||
|
||
// 4) 命名 + Save(键盘用 Return 收;Save 用 element click 不受键盘遮挡)
|
||
const tf = await driver.findElementRaw('class name', 'XCUIElementTypeTextField').catch(() => null);
|
||
if (tf) {
|
||
await driver.tapElement(tf); await sleep(400);
|
||
await driver.typeText(tf, cfg.name); await sleep(400);
|
||
for (const k of ['Return', 'Done', 'done', '完成']) { const kb = await driver.findElementRaw('name', k).catch(() => null); if (kb) { await driver.tapElement(kb); break; } }
|
||
await sleep(800);
|
||
}
|
||
await tapNameIOS(driver, 'Save'); await sleep(2500);
|
||
await dismissSaveConfirmCancel(driver).catch(() => {}); // 若弹"立即执行一次?"→ Cancel
|
||
await sleep(1500);
|
||
|
||
// 5) 校验:回 Automations 列表查名
|
||
let ok = false;
|
||
for (let i = 0; i < 6 && !ok; i++) {
|
||
await gotoAutomationsTabIOS(driver, W, H);
|
||
ok = (await driver.getSource()).includes(cfg.name);
|
||
if (!ok) await sleep(1500);
|
||
}
|
||
console.log(`创建自动化(iOS) ${cfg.name}: ${ok}`);
|
||
return ok;
|
||
}
|
||
|
||
/** 创建定时自动化:条件=Schedules(targetH:targetM)+ 动作=设备动作。返回是否创建成功(出现在 My Automations)。 */
|
||
export async function createAutomation(driver: DeviceDriver, cfg: AutomationConfig): Promise<boolean> {
|
||
if (driver.platform === 'ios') return createAutomationIOS(driver, cfg);
|
||
await driver.dismissPopupIfPresent().catch(() => {}); // 清启动 Tips/SmartThings 等弹框
|
||
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (auto) { await driver.tapElement(auto); await sleep(2500); }
|
||
await driver.dismissPopupIfPresent().catch(() => {});
|
||
if (!(await tapId(driver, 'addBto'))) { console.log('FAIL: no Create(addBto)'); return false; }
|
||
await sleep(2500);
|
||
await dismissGuideOverlay(driver); // 首次引导浮层
|
||
|
||
// 条件:Add condition → Schedules → 时间轮 → Save。条件页可能稍慢/有引导,重试等待。
|
||
let onCond = false;
|
||
for (let i = 0; i < 4 && !onCond; i++) {
|
||
await dismissGuideOverlay(driver);
|
||
await tapContains(driver, 'Add condition');
|
||
await sleep(2000);
|
||
for (let j = 0; j < 6; j++) {
|
||
if ((await driver.getSource()).includes('Schedules')) { onCond = true; break; }
|
||
await sleep(800);
|
||
}
|
||
}
|
||
if (!onCond) { console.log('FAIL: 条件类型页无 Schedules'); return false; }
|
||
await tapText(driver, 'Schedules');
|
||
await sleep(2000);
|
||
await setScheduleTime(driver, cfg.targetHour, cfg.targetMinute);
|
||
await tapText(driver, 'Save'); // Schedules 页 Save
|
||
await sleep(2500);
|
||
|
||
// 动作:Add action → Smart Devices → 设备 → 动作
|
||
await tapContains(driver, 'Add action');
|
||
await sleep(2000);
|
||
if (!(await selectSmartDeviceAction(driver, cfg.deviceKeyword, cfg.action))) return false;
|
||
|
||
// 命名 + Save
|
||
const ed = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Enter Automation name")');
|
||
if (ed) { await driver.tapElement(ed); await sleep(300); await driver.typeText(ed, cfg.name); await sleep(300); try { await driver.goBack(); } catch { /**/ } }
|
||
await tapText(driver, 'Save');
|
||
await sleep(2500);
|
||
await dismissSaveConfirmCancel(driver); // Save 后弹"立即执行一次?"确认框 → 点 Cancel(自动化已保存)
|
||
await driver.dismissPopupIfPresent();
|
||
|
||
// 校验:回 My Automations 列表重试查名(Save 后可能先 Loading/停详情页,不能立即判)
|
||
let ok = false;
|
||
for (let i = 0; i < 6 && !ok; i++) {
|
||
const a = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (a) { await driver.tapElement(a); await sleep(2000); }
|
||
ok = (await driver.getSource()).includes(cfg.name);
|
||
if (!ok) await sleep(2000);
|
||
}
|
||
console.log(`创建自动化 ${cfg.name}: ${ok}`);
|
||
return ok;
|
||
}
|
||
|
||
/** 打开某自动化(确保在 My Automations 列表后再点名字,避免在日志页点到日志条目)。 */
|
||
async function openAutomation(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
// 先退出可能的子页(Automation Logs / 详情),回到能看到底部导航的页
|
||
for (let i = 0; i < 3; i++) {
|
||
const s = await driver.getSource();
|
||
if (s.includes('Automation Logs') || s.includes('Scene Logs') || s.includes('Edit Automation View')) {
|
||
await driver.goBack(); await sleep(1000);
|
||
} else break;
|
||
}
|
||
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (auto) { await driver.tapElement(auto); await sleep(2000); }
|
||
// 确认在 My Automations 列表页(非日志页)再点名字
|
||
if (!(await driver.getSource()).includes('My Automations')) {
|
||
const auto2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (auto2) { await driver.tapElement(auto2); await sleep(2000); }
|
||
}
|
||
return tapContains(driver, name);
|
||
}
|
||
|
||
/**
|
||
* 验证自动化已执行:**自动化管理页(My Automations)右上角图标 → 菜单 "Automation Logs"** → 看是否有执行记录。
|
||
* 注意:日志入口在管理页右上角(全局日志),不是自动化详情页。
|
||
*/
|
||
export async function verifyAutomationExecuted(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
if (driver.platform === 'ios') return verifyAutomationExecutedIOS(driver, name);
|
||
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (auto) { await driver.tapElement(auto); await sleep(2500); }
|
||
// 右上角图标 → 菜单
|
||
const ic = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/top_bar_right_icon');
|
||
if (ic) await driver.tapElement(ic); else await driver.tap(1000, 180);
|
||
await sleep(1500);
|
||
if (!(await tapText(driver, 'Automation Logs'))) { console.log('未进入 Automation Logs'); return false; }
|
||
await sleep(2500);
|
||
const src = await driver.getSource();
|
||
// 有该自动化名的记录,或日志非空(非 "No logs / 暂无")
|
||
const hasLog = src.includes(name) || (!src.includes('No logs') && !src.includes('暂无') && !src.includes('No Logs'));
|
||
console.log(`Automation Logs 有执行记录(${name}): ${hasLog}`);
|
||
return hasLog;
|
||
}
|
||
|
||
/** 删除自动化:打开 → Edit Automation 的 Delete 按钮 → 确认框 Delete。返回是否已删除。 */
|
||
export async function deleteAutomation(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
if (driver.platform === 'ios') return deleteAutomationIOS(driver, name);
|
||
if (!(await openAutomation(driver, name))) return true;
|
||
await sleep(2000);
|
||
// Edit Automation 页底部/右上的 Delete
|
||
if (!(await tapText(driver, 'Delete'))) {
|
||
const del = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Delete")');
|
||
if (del) await driver.tapElement(del);
|
||
}
|
||
// 等确认框("Cancel | Delete")出现,点对话框里的 Delete(最后一个)
|
||
for (let i = 0; i < 6; i++) {
|
||
const dels = await driver.findElementsRaw('-android uiautomator', 'new UiSelector().text("Delete")');
|
||
const src = await driver.getSource();
|
||
if (src.includes('Cancel') && dels.length) { await driver.tapElement(dels[dels.length - 1]); break; }
|
||
await sleep(800);
|
||
}
|
||
await sleep(3000);
|
||
await driver.dismissPopupIfPresent();
|
||
// gone 校验:回 My Automations 列表查名
|
||
const a = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
|
||
if (a) { await driver.tapElement(a); await sleep(2000); }
|
||
const gone = !(await driver.getSource()).includes(name);
|
||
console.log(`删除自动化 ${name}: ${gone ? '已删' : '仍在'}`);
|
||
return gone;
|
||
}
|
||
|
||
// ============ iOS verify / delete(2026-06-23,待实跑细化:Automation Logs 入口 + 详情删除) ============
|
||
|
||
/** iOS 验证执行:Automations tab → 右上 More → Automation Logs → 看是否有该自动化执行记录。 */
|
||
async function verifyAutomationExecutedIOS(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
const { width: W, height: H } = await driver.getWindowSize().catch(() => ({ width: 390, height: 844 }));
|
||
await gotoAutomationsTabIOS(driver, W, H);
|
||
const more = await driver.findElementRaw('name', 'More').catch(() => null);
|
||
if (more) { await driver.tapElement(more); await sleep(1500); }
|
||
for (const t of ['Automation Logs', 'Logs', '自动化日志', '日志']) {
|
||
const el = await driver.findElementRaw('predicate string', `name CONTAINS "${t}" OR label CONTAINS "${t}"`).catch(() => null);
|
||
if (el) { await driver.tapElement(el); break; }
|
||
}
|
||
await sleep(2500);
|
||
const src = await driver.getSource();
|
||
const has = src.includes(name) || (!src.includes('No logs') && !src.includes('No Logs') && !src.includes('暂无') && src.includes('Bot'));
|
||
console.log(`Automation Logs(iOS) 有执行记录(${name}): ${has}`);
|
||
return has;
|
||
}
|
||
|
||
/** iOS 删除:复位 → Automations tab → 点自动化名进 Edit 详情 → 右上 Delete → 确认框 Delete(宽按钮,点中心)。 */
|
||
async function deleteAutomationIOS(driver: DeviceDriver, name: string): Promise<boolean> {
|
||
const { width: W, height: H } = await driver.getWindowSize().catch(() => ({ width: 390, height: 844 }));
|
||
// verify 后常停在 Automation Logs 子页 → 先复位到干净态再进列表(否则从子页起步删除会失败)。
|
||
await driver.restartApp?.().catch(() => {});
|
||
await sleep(2500);
|
||
await driver.dismissPopupIfPresent().catch(() => {});
|
||
await gotoAutomationsTabIOS(driver, W, H);
|
||
const el = await driver.findElementRaw('predicate string', `name CONTAINS "${name}" OR label CONTAINS "${name}"`).catch(() => null);
|
||
if (!el) { console.log(`删除自动化(iOS) ${name}: 不在列表(视为已删)`); return true; }
|
||
{ const r = await driver.getElementRect(el); await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); }
|
||
await sleep(2000);
|
||
// Edit Automation 页右上 "Delete"(窄,@~326,60)→ 弹确认框
|
||
const topDel = await driver.findElementRaw('predicate string', 'name == "Delete" OR label == "Delete"').catch(() => null);
|
||
if (topDel) { await driver.tapElement(topDel); await sleep(1500); }
|
||
// 确认框里有 Cancel + Delete(宽按钮 width>80);点宽 Delete 的 rect 中心(tapElement)。
|
||
for (let i = 0; i < 4; i++) {
|
||
const dels = await driver.findElementsRaw('predicate string', 'name == "Delete" OR label == "Delete"').catch(() => [] as string[]);
|
||
let tapped = false;
|
||
for (const d of dels) {
|
||
const r = await driver.getElementRect(d).catch(() => null);
|
||
if (r && r.width > 80) { await driver.tapElement(d); tapped = true; break; } // 确认按钮(非右上窄 Delete)
|
||
}
|
||
if (tapped) break;
|
||
await sleep(700);
|
||
}
|
||
await sleep(2000);
|
||
await gotoAutomationsTabIOS(driver, W, H);
|
||
const gone = !(await driver.getSource()).includes(name);
|
||
console.log(`删除自动化(iOS) ${name}: ${gone ? '已删' : '仍在'}`);
|
||
return gone;
|
||
}
|