/** * adb method: input — 通过 ADB Keyboard 输入文本(先安装 static/ADBKeyboard.apk,切换 IME,发送后切回原输入法) */ const path = require('path') const fs = require('fs') const { execSync, spawnSync } = require('child_process') const ADB_KEYBOARD_IME_ID = 'com.android.adbkeyboard/.AdbKeyboard' const ADB_KEYBOARD_PACKAGE = 'com.android.adbkeyboard' const B64_CHUNK_CHARS = 200 function getProjectRoot(ctx) { const root = ctx.compilerConfig?.projectRoot if (root && fs.existsSync(root)) return root // 本文件在 .../nodejs/ef-compiler/actions/fun/adb:上溯 **5** 层才到仓库根(4 层只会停在 nodejs/) return path.resolve(__dirname, '..', '..', '..', '..', '..') } function getAdbPath(projectRoot) { try { const configPath = process.env.STATIC_ROOT ? path.join(path.dirname(path.resolve(process.env.STATIC_ROOT)), 'config.js') : path.join(projectRoot, 'config.js') const config = fs.existsSync(configPath) ? require(configPath) : {} const cfgRoot = (config.projectRoot && fs.existsSync(config.projectRoot)) ? config.projectRoot : projectRoot const p = config.adbPath?.path if (p) return path.isAbsolute(p) ? p : path.resolve(cfgRoot, p) } catch (e) {} return path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb') } function getAdbKeyboardApkPath(projectRoot) { const p = path.join(projectRoot, 'static', 'ADBKeyboard.apk') return fs.existsSync(p) ? p : null } function deviceArgs(device) { return device && String(device).includes(':') ? ['-s', device] : [] } function runShell(adbPath, device, shellArgs, timeout = 3000) { const args = [...deviceArgs(device), 'shell', shellArgs] const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout }) if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb shell failed: ${r.status}`) return (r.stdout || '').trim() } function runShellQuiet(adbPath, device, shellArgs, timeout = 3000) { const args = [...deviceArgs(device), 'shell', shellArgs] const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout }) return r.status === 0 } function isPackageInstalled(adbPath, device, packageId) { try { const out = runShell(adbPath, device, `pm list packages ${packageId}`, 3000) return (out || '').indexOf('package:' + packageId) >= 0 } catch (e) { return false } } function installApk(adbPath, device, apkPath) { const args = [...deviceArgs(device), 'install', '-r', apkPath] const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 60000 }) if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb install failed: ${r.status}`) } function getCurrentIme(adbPath, device) { try { return runShell(adbPath, device, 'settings get secure default_input_method', 3000) } catch (e) { return '' } } function listEnabledImes(adbPath, device) { try { const out = runShell(adbPath, device, 'ime list -s', 3000) return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) } catch (e) { return [] } } function listAllImes(adbPath, device) { try { const out = runShell(adbPath, device, 'ime list -a', 3000) return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) } catch (e) { return [] } } /** 从 ime list -a 输出解析 IME id 列表(行格式多为 "com.pkg/.Class:") */ function parseImeIdsFromListA(out) { const ids = [] const lines = (out || '').split(/\r?\n/) for (const line of lines) { const t = line.trim() if (t.includes('/') && (t.endsWith(':') || !t.includes(' '))) { ids.push(t.replace(/:$/, '')) } } return ids } function findAdbKeyboardImeId(adbPath, device) { try { const out = runShell(adbPath, device, 'ime list -a', 3000) const ids = parseImeIdsFromListA(out) const lower = s => String(s).toLowerCase() const found = ids.find(id => lower(id).includes('adbkeyboard') || lower(id).includes('adb.keyboard')) return found || ADB_KEYBOARD_IME_ID } catch (e) { return ADB_KEYBOARD_IME_ID } } /** 从 dumpsys package 解析包内 BroadcastReceiver 组件名,用于 -n 显式发送 */ function getPackageReceivers(adbPath, device, packageId) { try { const out = runShell(adbPath, device, `dumpsys package ${packageId}`, 8000) const receivers = [] const re = /ComponentInfo\{([^}]+)\}/g let m while ((m = re.exec(out)) !== null) { const comp = m[1].trim() if (comp.startsWith(packageId + '/')) receivers.push(comp) } return [...new Set(receivers)] } catch (e) { return [] } } function enableIme(adbPath, device, imeId) { if (!imeId) return const args = [...deviceArgs(device), 'shell', 'ime', 'enable', imeId] const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 }) if (r.status !== 0 && r.stderr) { throw new Error(`ime enable 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`) } } function setIme(adbPath, device, imeId) { if (!imeId) return const args = [...deviceArgs(device), 'shell', 'ime', 'set', imeId] const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 }) if (r.status !== 0) { throw new Error(`ime set 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`) } } function ensureAdbKeyboardInstalled(adbPath, device, projectRoot) { const apkPath = getAdbKeyboardApkPath(projectRoot) if (!apkPath) throw new Error('未找到 static/ADBKeyboard.apk,请将 ADB Keyboard 的 apk 放在该路径') if (!isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE)) installApk(adbPath, device, apkPath) } /** 单次 broadcast(--es 字符串),返回是否成功 */ function tryBroadcast(adbPath, device, action, key, value, component) { const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', action, '--es', key, value] if (component) args.splice(args.indexOf(action) + 1, 0, '-n', component) const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 }) return r.status === 0 } /** ADB_INPUT_CHARS:--eia chars 码点1 码点2 ...(部分设备 B64 异常时可用) */ function tryBroadcastChars(adbPath, device, codePoints, component) { const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', 'ADB_INPUT_CHARS', '--eia', 'chars', ...codePoints.map(String)] if (component) args.splice(args.indexOf('ADB_INPUT_CHARS') + 1, 0, '-n', component) const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 }) return r.status === 0 } /** 依次尝试多种方式发送,直到一种成功。先试隐式(系统派发到注册的 Receiver),再试 -n */ async function sendTextViaBroadcast(adbPath, device, str, adbKeyboardImeId, logMessage, folderPath) { const fromDumpsys = getPackageReceivers(adbPath, device, ADB_KEYBOARD_PACKAGE) const components = [ null, adbKeyboardImeId, ...fromDumpsys, `${ADB_KEYBOARD_PACKAGE}/.AdbReceiver`, `${ADB_KEYBOARD_PACKAGE}/.Receiver` ] const dedup = [...new Set(components)] for (let i = 0; i < str.length; i += B64_CHUNK_CHARS) { const chunk = str.slice(i, i + B64_CHUNK_CHARS) const b64 = Buffer.from(chunk, 'utf8').toString('base64') let sent = false let used = null for (const comp of dedup) { if (tryBroadcast(adbPath, device, 'ADB_INPUT_B64', 'msg', b64, comp)) { sent = true used = comp ? comp : 'B64隐式' break } } if (!sent && chunk.length <= 100) { const codePoints = [...chunk].map(c => c.codePointAt(0)) for (const comp of [adbKeyboardImeId, null]) { if (tryBroadcastChars(adbPath, device, codePoints, comp)) { sent = true used = 'ADB_INPUT_CHARS' break } } } if (!sent && tryBroadcast(adbPath, device, 'ADB_INPUT_TEXT', 'msg', chunk, null)) { sent = true used = 'ADB_INPUT_TEXT' } if (!sent) throw new Error('broadcast 所有方式均失败') if (i + B64_CHUNK_CHARS < str.length) await new Promise(r => setTimeout(r, 150)) } } async function run(action, ctx) { const { device, folderPath, variableContext, api, extractVarName, resolveValue } = ctx let inputValue = null const inVars = action.inVars || [] if (inVars.length > 0) { const raw = inVars[0] if (raw != null && typeof raw === 'string' && !(raw.startsWith('{') && raw.endsWith('}'))) { inputValue = raw } else { inputValue = variableContext[extractVarName(raw)] } } if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext) if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' } if (action.clear) { if (!api?.sendKeyEvent) return { success: false, error: '清空需要 sendKeyEvent API' } for (let i = 0; i < 200; i++) { const clearResult = await api.sendKeyEvent(device, '67') if (!clearResult.success) break await new Promise((r) => setTimeout(r, 10)) } await new Promise((r) => setTimeout(r, 200)) } const projectRoot = getProjectRoot(ctx) const adbPath = getAdbPath(projectRoot) if (!fs.existsSync(adbPath)) return { success: false, error: `未找到 adb: ${adbPath}` } const logMessage = ctx.logMessage const needInstall = !isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE) if (needInstall) { ensureAdbKeyboardInstalled(adbPath, device, projectRoot) } const adbKeyboardImeId = findAdbKeyboardImeId(adbPath, device) try { const prevIme = getCurrentIme(adbPath, device) try { try { enableIme(adbPath, device, adbKeyboardImeId) } catch (enableErr) {} await new Promise(r => setTimeout(r, 400)) try { const sizeOut = runShell(adbPath, device, 'wm size', 3000) const m = (sizeOut || '').match(/(\d+)x(\d+)/) if (m) { const w = parseInt(m[1], 10) const h = parseInt(m[2], 10) const tapX = Math.floor(w / 2) const tapY = Math.floor(h * 0.28) runShellQuiet(adbPath, device, `input tap ${tapX} ${tapY}`, 2000) await new Promise(r => setTimeout(r, 500)) } } catch (e) {} setIme(adbPath, device, adbKeyboardImeId) await new Promise(r => setTimeout(r, 1000)) await sendTextViaBroadcast(adbPath, device, String(inputValue), adbKeyboardImeId, logMessage, folderPath) await new Promise(r => setTimeout(r, 1000)) } finally { if (prevIme && prevIme !== adbKeyboardImeId) { setIme(adbPath, device, prevIme) } else { const enabled = listEnabledImes(adbPath, device) const other = enabled.find(id => id !== adbKeyboardImeId) if (other) setIme(adbPath, device, other) } } } catch (e) { const msg = e && (e.message || String(e)) || 'unknown' return { success: false, error: `输入失败: ${msg}` } } return { success: true } } module.exports = { run }