|
1561 | 1561 | _loadExecutionPolicyOverrides(); |
1562 | 1562 | _loadErrorClassificationOverrides(); |
1563 | 1563 | _loadCustomActionTypeOverrides(); |
| 1564 | + _loadAppMappingOverrides(); |
| 1565 | + _loadCommandPatternOverrides(); |
| 1566 | +} |
| 1567 | + |
| 1568 | + |
| 1569 | +/* ════════════════════════════════════════════════════════ |
| 1570 | + APP MAPPINGS – ported from AppMappings.kt (static table) |
| 1571 | + Provides a fast, native-call-free resolution for well-known apps. |
| 1572 | + Anything NOT in this table still falls back to the native |
| 1573 | + Bridge.openAppByNameOrPackage(), which fuzzy-matches against the |
| 1574 | + device's actually-installed apps via PackageManager (JS cannot |
| 1575 | + enumerate installed apps - that part must stay native). |
| 1576 | +════════════════════════════════════════════════════════ */ |
| 1577 | +const _appDefinitions = [ |
| 1578 | + { name: "whatsapp", pkg: "com.whatsapp", variations: ["whats app","whats","wa"] }, |
| 1579 | + { name: "facebook", pkg: "com.facebook.katana", variations: ["fb","face book"] }, |
| 1580 | + { name: "messenger", pkg: "com.facebook.orca", variations: [] }, |
| 1581 | + { name: "instagram", pkg: "com.instagram.android", variations: ["insta","ig"] }, |
| 1582 | + { name: "youtube", pkg: "com.google.android.youtube", variations: ["yt","you tube"] }, |
| 1583 | + { name: "twitter", pkg: "com.twitter.android", variations: ["x","tweet"], aliases: ["x"] }, |
| 1584 | + { name: "telegram", pkg: "org.telegram.messenger", variations: ["tg"] }, |
| 1585 | + { name: "tiktok", pkg: "com.zhiliaoapp.musically", variations: ["tik tok"] }, |
| 1586 | + { name: "snapchat", pkg: "com.snapchat.android", variations: ["snap"] }, |
| 1587 | + { name: "netflix", pkg: "com.netflix.mediaclient", variations: ["nflx"] }, |
| 1588 | + { name: "spotify", pkg: "com.spotify.music", variations: ["music"] }, |
| 1589 | + { name: "chrome", pkg: "com.android.chrome", variations: ["google chrome","browser"] }, |
| 1590 | + { name: "gmail", pkg: "com.google.android.gm", variations: ["google mail","email","mail"] }, |
| 1591 | + { name: "maps", pkg: "com.google.android.apps.maps", variations: ["google maps","navigation"] }, |
| 1592 | + { name: "calendar", pkg: "com.google.android.calendar", variations: ["google calendar"] }, |
| 1593 | + { name: "photos", pkg: "com.google.android.apps.photos", variations: ["google photos","gallery"] }, |
| 1594 | + { name: "drive", pkg: "com.google.android.apps.docs", variations: ["google drive"] }, |
| 1595 | + { name: "docs", pkg: "com.google.android.apps.docs.editors.docs", variations: ["google docs","documents"] }, |
| 1596 | + { name: "sheets", pkg: "com.google.android.apps.docs.editors.sheets", variations: ["google sheets","spreadsheets"] }, |
| 1597 | + { name: "slides", pkg: "com.google.android.apps.docs.editors.slides", variations: ["google slides","presentations"] }, |
| 1598 | + { name: "keep", pkg: "com.google.android.keep", variations: ["google keep","notes"] }, |
| 1599 | + { name: "amazon", pkg: "com.amazon.mShop.android.shopping", variations: ["amazon shopping","shop"] }, |
| 1600 | + { name: "uber", pkg: "com.ubercab", variations: ["uber driver"] }, |
| 1601 | + { name: "lyft", pkg: "me.lyft.android", variations: ["ride"] }, |
| 1602 | + { name: "doordash", pkg: "com.dd.doordash", variations: ["food delivery"] }, |
| 1603 | + { name: "ubereats", pkg: "com.ubercab.eats", variations: ["uber eats","food"] }, |
| 1604 | + { name: "grubhub", pkg: "com.grubhub.android", variations: ["food delivery"] }, |
| 1605 | + { name: "instacart", pkg: "com.instacart.client", variations: ["grocery"] }, |
| 1606 | + { name: "zoom", pkg: "us.zoom.videomeetings", variations: ["zoom meeting"] }, |
| 1607 | + { name: "teams", pkg: "com.microsoft.teams", variations: ["microsoft teams"] }, |
| 1608 | + { name: "skype", pkg: "com.skype.raider", variations: ["microsoft skype"] }, |
| 1609 | + { name: "outlook", pkg: "com.microsoft.office.outlook", variations: ["microsoft outlook","email"] }, |
| 1610 | + { name: "word", pkg: "com.microsoft.office.word", variations: ["microsoft word","documents"] }, |
| 1611 | + { name: "excel", pkg: "com.microsoft.office.excel", variations: ["microsoft excel","spreadsheets"] }, |
| 1612 | + { name: "powerpoint", pkg: "com.microsoft.office.powerpoint", variations: ["microsoft powerpoint","presentations"] }, |
| 1613 | + { name: "onedrive", pkg: "com.microsoft.skydrive", variations: ["microsoft onedrive","cloud"] }, |
| 1614 | + { name: "onenote", pkg: "com.microsoft.office.onenote", variations: ["microsoft onenote","notes"] }, |
| 1615 | + { name: "linkedin", pkg: "com.linkedin.android", variations: ["linked in"] }, |
| 1616 | + { name: "pinterest", pkg: "com.pinterest", variations: ["pin"] }, |
| 1617 | + { name: "reddit", pkg: "com.reddit.frontpage", variations: ["reddit app"] }, |
| 1618 | + { name: "discord", pkg: "com.discord", variations: ["chat"] }, |
| 1619 | + { name: "slack", pkg: "com.Slack", variations: ["work chat"] }, |
| 1620 | + { name: "twitch", pkg: "tv.twitch.android.app", variations: ["streaming"] }, |
| 1621 | + { name: "venmo", pkg: "com.venmo", variations: ["payment"] }, |
| 1622 | + { name: "paypal", pkg: "com.paypal.android.p2pmobile", variations: ["payment"] }, |
| 1623 | + { name: "cashapp", pkg: "com.squareup.cash", variations: ["cash app","payment"] }, |
| 1624 | + { name: "zelle", pkg: "com.zellepay.zelle", variations: ["payment"] }, |
| 1625 | + { name: "robinhood", pkg: "com.robinhood.android", variations: ["stocks"] }, |
| 1626 | + { name: "coinbase", pkg: "com.coinbase.android", variations: ["crypto"] }, |
| 1627 | + { name: "binance", pkg: "com.binance.dev", variations: ["crypto"] }, |
| 1628 | + { name: "wechat", pkg: "com.tencent.mm", variations: ["we chat"] }, |
| 1629 | + { name: "line", pkg: "jp.naver.line.android", variations: ["line messenger"] }, |
| 1630 | + { name: "viber", pkg: "com.viber.voip", variations: ["viber messenger"] }, |
| 1631 | + { name: "signal", pkg: "org.thoughtcrime.securesms", variations: ["signal messenger"] }, |
| 1632 | + { name: "threema", pkg: "ch.threema.app", variations: ["threema messenger"] }, |
| 1633 | + { name: "settings", pkg: "com.android.settings", variations: ["system settings","preferences"] }, |
| 1634 | +]; |
| 1635 | + |
| 1636 | +/* ── AppMappingOverridesConfig (remote JSON, same file/pattern) ─ */ |
| 1637 | +let _appMappingOverrides = { matchThreshold: 70, apps: [] }; |
| 1638 | + |
| 1639 | +function _loadAppMappingOverrides() { |
| 1640 | + fetch('app-mappings-overrides.json', { cache: 'no-store' }) |
| 1641 | + .then(r => r.ok ? r.text() : null) |
| 1642 | + .then(json => { |
| 1643 | + if (!json) return; |
| 1644 | + try { |
| 1645 | + const obj = JSON.parse(json); |
| 1646 | + const threshold = Number(obj.matchThreshold); |
| 1647 | + _appMappingOverrides.matchThreshold = |
| 1648 | + (!isNaN(threshold) && threshold >= 0 && threshold <= 100) ? threshold : 70; |
| 1649 | + _appMappingOverrides.apps = Array.isArray(obj.apps) |
| 1650 | + ? obj.apps |
| 1651 | + .filter(a => a && typeof a.canonicalName === 'string' && a.canonicalName.trim() |
| 1652 | + && typeof a.packageName === 'string' && a.packageName.trim()) |
| 1653 | + .map(a => ({ |
| 1654 | + name: a.canonicalName.trim(), |
| 1655 | + pkg: a.packageName.trim(), |
| 1656 | + variations: Array.isArray(a.variations) ? a.variations.filter(v => typeof v === 'string' && v.trim()) : [], |
| 1657 | + aliases: Array.isArray(a.aliasesForPackageLookup) ? a.aliasesForPackageLookup.filter(v => typeof v === 'string' && v.trim()) : [], |
| 1658 | + })) |
| 1659 | + : []; |
| 1660 | + } catch(e) { console.warn('app-mappings-overrides.json parse error', e); } |
| 1661 | + }) |
| 1662 | + .catch(() => { /* optional file */ }); |
| 1663 | +} |
| 1664 | + |
| 1665 | +/** |
| 1666 | + * Fast, native-call-free lookup: normalized name/variation/alias → package name. |
| 1667 | + * Overrides win on a canonicalName clash (merged on top of built-ins), mirroring |
| 1668 | + * AppMappings.kt's `builtIn + overrides` behavior. |
| 1669 | + */ |
| 1670 | +function _resolveKnownAppPackage(rawName) { |
| 1671 | + const normalize = (s) => (s || '').toLowerCase().trim().replace(/[^a-z0-9]/g, ''); |
| 1672 | + const norm = normalize(rawName); |
| 1673 | + if (!norm) return null; |
| 1674 | + if (norm.includes('.')) return norm; // already looks like a package name |
| 1675 | + |
| 1676 | + const table = new Map(); |
| 1677 | + for (const def of _appDefinitions) { |
| 1678 | + table.set(normalize(def.name), def.pkg); |
| 1679 | + (def.aliases || []).forEach(a => table.set(normalize(a), def.pkg)); |
| 1680 | + (def.variations || []).forEach(v => table.set(normalize(v), def.pkg)); |
| 1681 | + } |
| 1682 | + // Overrides win on clash |
| 1683 | + for (const def of _appMappingOverrides.apps) { |
| 1684 | + table.set(normalize(def.name), def.pkg); |
| 1685 | + (def.aliases || []).forEach(a => table.set(normalize(a), def.pkg)); |
| 1686 | + (def.variations || []).forEach(v => table.set(normalize(v), def.pkg)); |
| 1687 | + } |
| 1688 | + |
| 1689 | + return table.get(norm) || null; |
1564 | 1690 | } |
1565 | 1691 |
|
1566 | 1692 | /* ── PENDING STATE ───────────────────────────────────────────── */ |
|
1975 | 2101 | COMMAND PARSER |
1976 | 2102 | Ported from CommandParser.kt – parses AI responses for action commands. |
1977 | 2103 | ════════════════════════════════════════════════════════ */ |
1978 | | -function parseCommandsFromText(text) { |
| 2104 | +/** |
| 2105 | + * Mirrors CommandParser.kt's normalizeText(): collapses whitespace runs and |
| 2106 | + * normalizes line breaks before pattern matching, so multi-line / oddly |
| 2107 | + * formatted AI responses are recognized the same way as the native parser was. |
| 2108 | + */ |
| 2109 | +function _normalizeCommandText(text) { |
| 2110 | + return text.replace(/\s+/g, ' ').replace(/\r\n|\r/g, '\n').trim(); |
| 2111 | +} |
| 2112 | + |
| 2113 | +/* ── CommandPatternConfig (remote regex overrides for EXISTING command types) ── */ |
| 2114 | +// Maps a CommandType name (as used in command-patterns.json) to a builder function |
| 2115 | +// compatible with the JS command shape used by _executeSingleCommand/_cmdToString. |
| 2116 | +const _COMMAND_TYPE_BUILDERS = { |
| 2117 | + CLICK_BUTTON: (m) => ({ type: 'CLICK', text: m[1] }), |
| 2118 | + LONG_CLICK_BUTTON: (m) => ({ type: 'LONG_CLICK', text: m[1] }), |
| 2119 | + TAP_COORDINATES: (m) => ({ type: 'TAP', x: m[1], y: m[2] }), |
| 2120 | + TAKE_SCREENSHOT: () => ({ type: 'SCREENSHOT' }), |
| 2121 | + COMPLETED: () => ({ type: 'COMPLETED' }), |
| 2122 | + WAIT: (m) => ({ type: 'WAIT', seconds: parseInt(m[1], 10) }), |
| 2123 | + PRESS_HOME: () => ({ type: 'HOME' }), |
| 2124 | + PRESS_BACK: () => ({ type: 'BACK' }), |
| 2125 | + SHOW_RECENT_APPS: () => ({ type: 'RECENT_APPS' }), |
| 2126 | + SCROLL_DOWN: () => ({ type: 'SCROLL_DOWN' }), |
| 2127 | + SCROLL_UP: () => ({ type: 'SCROLL_UP' }), |
| 2128 | + SCROLL_LEFT: () => ({ type: 'SCROLL_LEFT' }), |
| 2129 | + SCROLL_RIGHT: () => ({ type: 'SCROLL_RIGHT' }), |
| 2130 | + SCROLL_DOWN_FROM_COORDINATES: (m) => ({ type: 'SCROLL_DOWN_C', x: m[1], y: m[2], dist: m[3], dur: parseInt(m[4], 10) }), |
| 2131 | + SCROLL_UP_FROM_COORDINATES: (m) => ({ type: 'SCROLL_UP_C', x: m[1], y: m[2], dist: m[3], dur: parseInt(m[4], 10) }), |
| 2132 | + SCROLL_LEFT_FROM_COORDINATES: (m) => ({ type: 'SCROLL_LEFT_C', x: m[1], y: m[2], dist: m[3], dur: parseInt(m[4], 10) }), |
| 2133 | + SCROLL_RIGHT_FROM_COORDINATES: (m) => ({ type: 'SCROLL_RIGHT_C', x: m[1], y: m[2], dist: m[3], dur: parseInt(m[4], 10) }), |
| 2134 | + OPEN_APP: (m) => ({ type: 'OPEN_APP', app: m[1] }), |
| 2135 | + WRITE_TEXT: (m) => ({ type: 'WRITE_TEXT', text: m[1] }), |
| 2136 | + USE_HIGH_REASONING_MODEL: () => null, // model switching ignored in JS, same as built-in patterns |
| 2137 | + USE_LOW_REASONING_MODEL: () => null, |
| 2138 | + PRESS_ENTER_KEY: () => ({ type: 'ENTER' }), |
| 2139 | + RETRIEVE: (m) => ({ type: 'RETRIEVE', title: m[1] }), |
| 2140 | + TERMUX_COMMAND: (m) => ({ type: 'TERMUX', command: m[2] ? m[2].replace(/\\(['"])/g, '$1') : m[1] }), |
| 2141 | + PINCH_GESTURE: (m) => ({ type: 'PINCH', cx: m[1], cy: m[2], sd: m[3], ed: m[4], dur: parseInt(m[5], 10) }), |
| 2142 | + COPY_TO_CLIPBOARD: (m) => ({ type: 'COPY_CLIP', text: m[1] }), |
| 2143 | +}; |
| 2144 | + |
| 2145 | +// Remote pattern overrides installed via command-patterns.json - same file/contract as the |
| 2146 | +// native CommandPatternConfig. Each entry attaches a new regex to an EXISTING command type's |
| 2147 | +// builder above; it can never introduce new logic, matching the native safety boundary. |
| 2148 | +let _remoteCommandPatterns = []; // [{id, commandType, regex}] |
| 2149 | + |
| 2150 | +function _loadCommandPatternOverrides() { |
| 2151 | + fetch('command-patterns.json', { cache: 'no-store' }) |
| 2152 | + .then(r => r.ok ? r.text() : null) |
| 2153 | + .then(json => { |
| 2154 | + if (!json) return; |
| 2155 | + try { |
| 2156 | + const arr = JSON.parse(json); |
| 2157 | + const parsed = []; |
| 2158 | + if (Array.isArray(arr)) { |
| 2159 | + for (let i = 0; i < arr.length; i++) { |
| 2160 | + const entry = arr[i]; |
| 2161 | + if (!entry || typeof entry.regex !== 'string' || !entry.regex.trim()) continue; |
| 2162 | + const typeName = entry.commandType; |
| 2163 | + if (!_COMMAND_TYPE_BUILDERS[typeName]) { |
| 2164 | + console.warn('Skipping command-patterns.json override: unknown commandType', typeName); |
| 2165 | + continue; |
| 2166 | + } |
| 2167 | + try { |
| 2168 | + parsed.push({ |
| 2169 | + id: entry.id || ('remote_' + i), |
| 2170 | + commandType: typeName, |
| 2171 | + regex: new RegExp(entry.regex, 'gi'), |
| 2172 | + }); |
| 2173 | + } catch(e) { console.warn('Invalid regex in command-patterns.json for', typeName, e); } |
| 2174 | + } |
| 2175 | + } |
| 2176 | + _remoteCommandPatterns = parsed; |
| 2177 | + } catch(e) { console.warn('command-patterns.json parse error', e); } |
| 2178 | + }) |
| 2179 | + .catch(() => { /* optional file */ }); |
| 2180 | +} |
| 2181 | + |
| 2182 | +function parseCommandsFromText(rawText) { |
| 2183 | + const text = _normalizeCommandText(rawText); |
1979 | 2184 | const commands = []; |
1980 | 2185 | const seenSingletons = new Set(); |
1981 | 2186 |
|
|
2078 | 2283 | } |
2079 | 2284 | } |
2080 | 2285 |
|
| 2286 | + // Remote pattern overrides (command-patterns.json) - extra regex syntaxes for EXISTING |
| 2287 | + // command types, using the same builder functions as the built-in patterns above. |
| 2288 | + for (const override of _remoteCommandPatterns) { |
| 2289 | + const builder = _COMMAND_TYPE_BUILDERS[override.commandType]; |
| 2290 | + if (!builder) continue; |
| 2291 | + override.regex.lastIndex = 0; |
| 2292 | + let m; |
| 2293 | + while ((m = override.regex.exec(text)) !== null) { |
| 2294 | + const cmd = builder(m); |
| 2295 | + if (cmd === null) continue; |
| 2296 | + const single = (override.commandType === 'TAKE_SCREENSHOT' || override.commandType === 'COMPLETED'); |
| 2297 | + allMatches.push({ start: m.index, end: m.index + m[0].length, cmd, type: override.commandType, single }); |
| 2298 | + } |
| 2299 | + } |
| 2300 | + |
2081 | 2301 | // Sort by position in text |
2082 | 2302 | allMatches.sort((a, b) => a.start - b.start); |
2083 | 2303 |
|
|
2156 | 2376 | case 'SCROLL_UP_C': Bridge.scrollUpFromCoordinates(cmd.x, cmd.y, cmd.dist, cmd.dur); break; |
2157 | 2377 | case 'SCROLL_LEFT_C': Bridge.scrollLeftFromCoordinates(cmd.x, cmd.y, cmd.dist, cmd.dur); break; |
2158 | 2378 | case 'SCROLL_RIGHT_C': Bridge.scrollRightFromCoordinates(cmd.x, cmd.y, cmd.dist, cmd.dur); break; |
2159 | | - case 'OPEN_APP': Bridge.openAppByNameOrPackage(cmd.app); break; |
| 2379 | + case 'OPEN_APP': { |
| 2380 | + // Fast path: resolve well-known apps (AppMappings.kt table + remote overrides) without |
| 2381 | + // a native PackageManager round-trip. Anything unrecognized still falls back to the |
| 2382 | + // native fuzzy-match against actually-installed apps (JS cannot enumerate those). |
| 2383 | + const resolvedPkg = _resolveKnownAppPackage(cmd.app); |
| 2384 | + Bridge.openAppByNameOrPackage(resolvedPkg || cmd.app); |
| 2385 | + break; |
| 2386 | + } |
2160 | 2387 | case 'PINCH': Bridge.pinchGesture(cmd.cx, cmd.cy, cmd.sd, cmd.ed, cmd.dur); break; |
2161 | 2388 | case 'COPY_CLIP': Bridge.copyToClipboard(cmd.text); break; |
2162 | 2389 | case 'TOAST': Bridge.showToast(cmd.msg, false); break; |
|
0 commit comments