Compare commits
5 Commits
23
README.md
23
README.md
@@ -631,6 +631,29 @@ data: [DONE]
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.2.0 (2026-05-20)
|
||||
- **Full Internal Virtual Environment** — `BootstrapPlugin.setupVirtualEnv()` now provisions an app-contained Python venv under app storage, no external Termux app required
|
||||
- **In-App Module Installation** — `BootstrapPlugin.venvPipInstall()` installs Python modules directly into the internal venv
|
||||
- **New AI Automation Tags** — `[VENV_SETUP]` and `[VENV_PIP_INSTALL package]` available in coding and agentic modes
|
||||
- **Critical Runtime Fix** — replaced invalid JS `new File(...).exists` check with shell-based file existence test in PRoot strategy
|
||||
- **Concurrency Fix** — `ShellPlugin.activeProcesses` migrated to `ConcurrentHashMap` to avoid race/corruption under parallel process events
|
||||
- **Wake Stability** — added null-safe activity handling and power service guards in `WakePlugin`
|
||||
- **Bootstrap Resource Safety** — fixed major stream and descriptor leaks in download/extract paths (`downloadFile`, `extractBootstrap`)
|
||||
- **Accessibility Labels** — added missing `aria-label` attributes for icon-only controls in UI
|
||||
- **Version sync** — About screen and package/build versions updated to 3.2.0
|
||||
|
||||
### v3.1.0 (2026-05-20)
|
||||
- **AutoGLM Device Control** — `AccessibilityService` for full device automation: tap, swipe, long press, type text, screenshot, UI tree
|
||||
- **Navigation Controls** — press Back, Home, Recents, Notifications, Quick Settings, Power Dialog via global actions
|
||||
- **App Control** — launch apps, get current foreground app, read full accessibility node tree with bounds/text/clickable state
|
||||
- **Click by Text/ID** — `clickByText()` and `clickNode()` for semantic UI interaction without coordinates
|
||||
- **Hermes Agent** — `installHermes()` creates Python venv and installs hermes-agent via pip; `hermesExec()` runs Hermes commands
|
||||
- **AI Tool Tags** — `[DEVICE_*]` (17 tags) and `[HERMES_*]` (2 tags) parsed in both coding and agentic modes
|
||||
- **AutoGLMPlugin** — Capacitor plugin exposing all device control methods to JavaScript
|
||||
- **Accessibility Setup UI** — one-tap enable button in Dev Setup screen with live status indicator
|
||||
- **Hermes Install UI** — install button in Dev Setup screen
|
||||
- **v2 APK Signing** — release APK signed with APK Signature Scheme v2 (required for targetSdk 36)
|
||||
|
||||
### v2.3.0 (2026-05-20)
|
||||
- **Java Virtual Environment** — `ecj.jar` (3.2MB) and `apksigner.jar` (1.1MB) bundled as APK assets, extracted at runtime
|
||||
- **app_process Wrappers** — wrapper scripts use `/system/bin/app_process` to run JARs (bypasses SELinux `execve` restrictions entirely)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
def envOrDefault(String key, String fallback) {
|
||||
def fromEnv = System.getenv(key)
|
||||
if (fromEnv != null && fromEnv.trim()) return fromEnv.trim()
|
||||
if (project.hasProperty(key) && project.property(key)?.toString()?.trim()) return project.property(key).toString().trim()
|
||||
return fallback
|
||||
}
|
||||
|
||||
def debugStorePass = envOrDefault('ZAI_DEBUG_STORE_PASSWORD', 'android')
|
||||
def debugKeyAlias = envOrDefault('ZAI_DEBUG_KEY_ALIAS', 'androiddebugkey')
|
||||
def debugKeyPass = envOrDefault('ZAI_DEBUG_KEY_PASSWORD', 'android')
|
||||
def releaseStorePass = envOrDefault('ZAI_RELEASE_STORE_PASSWORD', 'zaichat')
|
||||
def releaseKeyAlias = envOrDefault('ZAI_RELEASE_KEY_ALIAS', 'zai-chat')
|
||||
def releaseKeyPass = envOrDefault('ZAI_RELEASE_KEY_PASSWORD', 'zaichat')
|
||||
|
||||
android {
|
||||
namespace = "ai.z.chat"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
@@ -7,8 +21,8 @@ android {
|
||||
applicationId "ai.z.chat"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 19
|
||||
versionName "2.3.0"
|
||||
versionCode 23
|
||||
versionName "3.2.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
@@ -18,15 +32,15 @@ android {
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
storePassword debugStorePass
|
||||
keyAlias debugKeyAlias
|
||||
keyPassword debugKeyPass
|
||||
}
|
||||
release {
|
||||
storeFile file('release.keystore')
|
||||
storePassword 'zaichat'
|
||||
keyAlias 'zai-chat'
|
||||
keyPassword 'zaichat'
|
||||
storePassword releaseStorePass
|
||||
keyAlias releaseKeyAlias
|
||||
keyPassword releaseKeyPass
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -36,6 +37,18 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".AutoGLMService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service_config" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
215
android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java
Normal file
215
android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package ai.z.chat;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import com.getcapacitor.annotation.Permission;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
|
||||
@CapacitorPlugin(
|
||||
name = "AutoGLM",
|
||||
permissions = {}
|
||||
)
|
||||
public class AutoGLMPlugin extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void isEnabled(PluginCall call) {
|
||||
call.resolve(new JSObject().put("enabled", AutoGLMService.isEnabled()));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void openSettings(PluginCall call) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
call.resolve(new JSObject().put("opened", true));
|
||||
} catch (Exception e) {
|
||||
call.reject("Cannot open accessibility settings: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void tap(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
int x = call.getInt("x", 0);
|
||||
int y = call.getInt("y", 0);
|
||||
svc.tap(x, y);
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void longPress(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
int x = call.getInt("x", 0);
|
||||
int y = call.getInt("y", 0);
|
||||
svc.longPress(x, y);
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void swipe(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
int startX = call.getInt("startX", 0);
|
||||
int startY = call.getInt("startY", 0);
|
||||
int endX = call.getInt("endX", 0);
|
||||
int endY = call.getInt("endY", 0);
|
||||
int duration = call.getInt("duration", 300);
|
||||
svc.swipe(startX, startY, endX, endY, duration);
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void typeText(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String text = call.getString("text", "");
|
||||
if (text.isEmpty()) { call.reject("text required"); return; }
|
||||
svc.typeText(text);
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pressBack(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
svc.pressBack();
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pressHome(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
svc.pressHome();
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pressRecents(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
svc.pressRecents();
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pressNotifications(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
svc.pressNotifications();
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void pressQuickSettings(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
svc.pressQuickSettings();
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void takeScreenshot(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
|
||||
call.reject("Screenshot requires Android 11+");
|
||||
return;
|
||||
}
|
||||
String destPath = call.getString("dest", "");
|
||||
if (destPath.isEmpty()) {
|
||||
destPath = getContext().getCacheDir() + "/autoglm_screenshot.png";
|
||||
}
|
||||
svc.takeScreenshot(destPath);
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
call.resolve(new JSObject().put("path", destPath).put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getUITree(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String tree = svc.getUITree();
|
||||
call.resolve(new JSObject().put("tree", tree).put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getFocusedNode(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String info = svc.getFocusedNodeInfo();
|
||||
call.resolve(new JSObject().put("node", info).put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void clickNode(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String viewId = call.getString("viewId", "");
|
||||
if (viewId.isEmpty()) { call.reject("viewId required"); return; }
|
||||
boolean result = svc.clickNode(viewId);
|
||||
call.resolve(new JSObject().put("ok", result));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void clickByText(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String text = call.getString("text", "");
|
||||
if (text.isEmpty()) { call.reject("text required"); return; }
|
||||
boolean result = svc.clickNodeByText(text);
|
||||
call.resolve(new JSObject().put("ok", result));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void scrollNode(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String viewId = call.getString("viewId", "");
|
||||
String direction = call.getString("direction", "forward");
|
||||
if (viewId.isEmpty()) { call.reject("viewId required"); return; }
|
||||
int action = direction.equals("forward")
|
||||
? android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
|
||||
: android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
|
||||
boolean result = svc.scrollNode(viewId, action);
|
||||
call.resolve(new JSObject().put("ok", result));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getCurrentApp(PluginCall call) {
|
||||
AutoGLMService svc = AutoGLMService.getInstance();
|
||||
if (svc == null) { call.reject("AutoGLM service not enabled"); return; }
|
||||
String pkg = svc.getCurrentApp();
|
||||
call.resolve(new JSObject().put("package", pkg).put("ok", true));
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void launchApp(PluginCall call) {
|
||||
String pkg = call.getString("package", "");
|
||||
if (pkg.isEmpty()) { call.reject("package required"); return; }
|
||||
try {
|
||||
Intent intent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
|
||||
if (intent != null) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
call.resolve(new JSObject().put("ok", true));
|
||||
} else {
|
||||
call.reject("Package not found: " + pkg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
call.reject("Launch failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
378
android/app/src/main/java/ai/z/chat/AutoGLMService.java
Normal file
378
android/app/src/main/java/ai/z/chat/AutoGLMService.java
Normal file
@@ -0,0 +1,378 @@
|
||||
package ai.z.chat;
|
||||
|
||||
import android.accessibilityservice.AccessibilityService;
|
||||
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||
import android.accessibilityservice.GestureDescription;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityWindowInfo;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AutoGLMService extends AccessibilityService {
|
||||
|
||||
private static final String TAG = "AutoGLMService";
|
||||
private static volatile AutoGLMService instance;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
Log.i(TAG, "AutoGLM AccessibilityService created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
instance = null;
|
||||
Log.i(TAG, "AutoGLM AccessibilityService destroyed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {}
|
||||
|
||||
@Override
|
||||
public void onInterrupt() {}
|
||||
|
||||
@Override
|
||||
protected void onServiceConnected() {
|
||||
super.onServiceConnected();
|
||||
instance = this;
|
||||
AccessibilityServiceInfo info = getServiceInfo();
|
||||
if (info == null) info = new AccessibilityServiceInfo();
|
||||
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
|
||||
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
|
||||
info.flags = AccessibilityServiceInfo.DEFAULT
|
||||
| AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
|
||||
| AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
|
||||
info.notificationTimeout = 100;
|
||||
setServiceInfo(info);
|
||||
Log.i(TAG, "AutoGLM service connected and configured");
|
||||
}
|
||||
|
||||
public static AutoGLMService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return instance != null;
|
||||
}
|
||||
|
||||
public void tap(int x, int y) {
|
||||
GestureDescription gesture = buildClick(x, y, 50);
|
||||
if (gesture != null) dispatchGesture(gesture, null, null);
|
||||
}
|
||||
|
||||
public void longPress(int x, int y) {
|
||||
GestureDescription gesture = buildClick(x, y, 500);
|
||||
if (gesture != null) dispatchGesture(gesture, null, null);
|
||||
}
|
||||
|
||||
public void swipe(int startX, int startY, int endX, int endY, int durationMs) {
|
||||
GestureDescription gesture = buildSwipe(startX, startY, endX, endY, durationMs);
|
||||
if (gesture != null) dispatchGesture(gesture, null, null);
|
||||
}
|
||||
|
||||
public void swipe(int startX, int startY, int endX, int endY) {
|
||||
swipe(startX, startY, endX, endY, 300);
|
||||
}
|
||||
|
||||
public void typeText(String text) {
|
||||
AccessibilityNodeInfo focusNode = findFocusNode();
|
||||
if (focusNode != null) {
|
||||
Bundle args = new Bundle();
|
||||
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
|
||||
focusNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
|
||||
focusNode.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void pressBack() {
|
||||
performGlobalAction(GLOBAL_ACTION_BACK);
|
||||
}
|
||||
|
||||
public void pressHome() {
|
||||
performGlobalAction(GLOBAL_ACTION_HOME);
|
||||
}
|
||||
|
||||
public void pressRecents() {
|
||||
performGlobalAction(GLOBAL_ACTION_RECENTS);
|
||||
}
|
||||
|
||||
public void pressNotifications() {
|
||||
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS);
|
||||
}
|
||||
|
||||
public void pressQuickSettings() {
|
||||
performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS);
|
||||
}
|
||||
|
||||
public void pressPowerDialog() {
|
||||
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
|
||||
}
|
||||
|
||||
public void pressLockScreen() {
|
||||
performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN);
|
||||
}
|
||||
|
||||
public void takeScreenshot(String destPath) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
takeScreenshot(android.view.Display.DEFAULT_DISPLAY,
|
||||
getMainExecutor(), new TakeScreenshotCallback() {
|
||||
@Override
|
||||
public void onSuccess(ScreenshotResult screenshot) {
|
||||
try {
|
||||
Bitmap bitmap = Bitmap.wrapHardwareBuffer(
|
||||
screenshot.getHardwareBuffer(), screenshot.getColorSpace());
|
||||
if (bitmap != null) {
|
||||
Bitmap softwareBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
try (FileOutputStream fos = new FileOutputStream(destPath)) {
|
||||
softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
|
||||
}
|
||||
softwareBitmap.recycle();
|
||||
}
|
||||
if (screenshot.getHardwareBuffer() != null) {
|
||||
screenshot.getHardwareBuffer().close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Screenshot save failed", e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(int errorCode) {
|
||||
Log.e(TAG, "Screenshot failed: " + errorCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public String getUITree() {
|
||||
try {
|
||||
JSONObject root = new JSONObject();
|
||||
JSONArray windowsArr = new JSONArray();
|
||||
|
||||
List<AccessibilityWindowInfo> windows = getWindows();
|
||||
for (AccessibilityWindowInfo window : windows) {
|
||||
JSONObject winObj = new JSONObject();
|
||||
winObj.put("id", window.getId());
|
||||
winObj.put("layer", window.getLayer());
|
||||
winObj.put("active", window.isActive());
|
||||
winObj.put("focused", window.isFocused());
|
||||
|
||||
AccessibilityNodeInfo rootNode = window.getRoot();
|
||||
if (rootNode != null) {
|
||||
winObj.put("root", serializeNode(rootNode));
|
||||
rootNode.recycle();
|
||||
}
|
||||
windowsArr.put(winObj);
|
||||
}
|
||||
|
||||
root.put("windows", windowsArr);
|
||||
root.put("windowCount", windows.size());
|
||||
return root.toString();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "getUITree failed", e);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
public String getFocusedNodeInfo() {
|
||||
try {
|
||||
AccessibilityNodeInfo focusNode = findFocusNode();
|
||||
if (focusNode != null) {
|
||||
JSONObject obj = serializeNode(focusNode);
|
||||
focusNode.recycle();
|
||||
return obj.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "getFocusedNodeInfo failed", e);
|
||||
}
|
||||
return "{}";
|
||||
}
|
||||
|
||||
public boolean clickNode(String viewId) {
|
||||
List<AccessibilityNodeInfo> nodes = findNodesByViewId(viewId);
|
||||
for (AccessibilityNodeInfo node : nodes) {
|
||||
if (node.isClickable()) {
|
||||
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
node.recycle();
|
||||
return true;
|
||||
}
|
||||
AccessibilityNodeInfo clickable = findClickableAncestor(node);
|
||||
if (clickable != null) {
|
||||
clickable.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
clickable.recycle();
|
||||
node.recycle();
|
||||
return true;
|
||||
}
|
||||
node.recycle();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean clickNodeByText(String text) {
|
||||
List<AccessibilityNodeInfo> nodes = findNodesByText(text);
|
||||
for (AccessibilityNodeInfo node : nodes) {
|
||||
if (node.isClickable()) {
|
||||
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
node.recycle();
|
||||
return true;
|
||||
}
|
||||
AccessibilityNodeInfo clickable = findClickableAncestor(node);
|
||||
if (clickable != null) {
|
||||
clickable.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
clickable.recycle();
|
||||
node.recycle();
|
||||
return true;
|
||||
}
|
||||
node.recycle();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean scrollNode(String viewId, int direction) {
|
||||
List<AccessibilityNodeInfo> nodes = findNodesByViewId(viewId);
|
||||
for (AccessibilityNodeInfo node : nodes) {
|
||||
boolean result = node.performAction(direction);
|
||||
node.recycle();
|
||||
if (result) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getCurrentApp() {
|
||||
try {
|
||||
List<AccessibilityWindowInfo> windows = getWindows();
|
||||
for (AccessibilityWindowInfo window : windows) {
|
||||
if (window.isActive()) {
|
||||
AccessibilityNodeInfo root = window.getRoot();
|
||||
if (root != null) {
|
||||
CharSequence pkg = root.getPackageName();
|
||||
root.recycle();
|
||||
return pkg != null ? pkg.toString() : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "getCurrentApp failed", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private AccessibilityNodeInfo findFocusNode() {
|
||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||
if (root == null) return null;
|
||||
|
||||
AccessibilityNodeInfo focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
|
||||
root.recycle();
|
||||
return focused;
|
||||
}
|
||||
|
||||
private List<AccessibilityNodeInfo> findNodesByViewId(String viewId) {
|
||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||
List<AccessibilityNodeInfo> result = new ArrayList<>();
|
||||
if (root != null) {
|
||||
result = root.findAccessibilityNodeInfosByViewId(viewId);
|
||||
root.recycle();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<AccessibilityNodeInfo> findNodesByText(String text) {
|
||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||
List<AccessibilityNodeInfo> result = new ArrayList<>();
|
||||
if (root != null) {
|
||||
result = root.findAccessibilityNodeInfosByText(text);
|
||||
root.recycle();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AccessibilityNodeInfo findClickableAncestor(AccessibilityNodeInfo node) {
|
||||
AccessibilityNodeInfo parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent.isClickable()) return parent;
|
||||
AccessibilityNodeInfo grandParent = parent.getParent();
|
||||
if (grandParent == null) {
|
||||
parent.recycle();
|
||||
return null;
|
||||
}
|
||||
parent.recycle();
|
||||
parent = grandParent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JSONObject serializeNode(AccessibilityNodeInfo node) throws Exception {
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("className", safeStr(node.getClassName()));
|
||||
obj.put("text", safeStr(node.getText()));
|
||||
obj.put("contentDesc", safeStr(node.getContentDescription()));
|
||||
obj.put("viewId", safeStr(node.getViewIdResourceName()));
|
||||
obj.put("packageName", safeStr(node.getPackageName()));
|
||||
obj.put("clickable", node.isClickable());
|
||||
obj.put("focusable", node.isFocusable());
|
||||
obj.put("editable", node.isEditable());
|
||||
obj.put("enabled", node.isEnabled());
|
||||
obj.put("checked", node.isChecked());
|
||||
obj.put("selected", node.isSelected());
|
||||
obj.put("scrollable", node.isScrollable());
|
||||
|
||||
Rect bounds = new Rect();
|
||||
node.getBoundsInScreen(bounds);
|
||||
JSONObject boundsObj = new JSONObject();
|
||||
boundsObj.put("left", bounds.left);
|
||||
boundsObj.put("top", bounds.top);
|
||||
boundsObj.put("right", bounds.right);
|
||||
boundsObj.put("bottom", bounds.bottom);
|
||||
obj.put("bounds", boundsObj);
|
||||
|
||||
int childCount = node.getChildCount();
|
||||
if (childCount > 0) {
|
||||
JSONArray children = new JSONArray();
|
||||
for (int i = 0; i < Math.min(childCount, 50); i++) {
|
||||
AccessibilityNodeInfo child = node.getChild(i);
|
||||
if (child != null) {
|
||||
children.put(serializeNode(child));
|
||||
child.recycle();
|
||||
}
|
||||
}
|
||||
obj.put("children", children);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private String safeStr(CharSequence cs) {
|
||||
return cs != null ? cs.toString() : "";
|
||||
}
|
||||
|
||||
private GestureDescription buildClick(int x, int y, long duration) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return null;
|
||||
Path path = new Path();
|
||||
path.moveTo(x, y);
|
||||
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, duration);
|
||||
return new GestureDescription.Builder().addStroke(stroke).build();
|
||||
}
|
||||
|
||||
private GestureDescription buildSwipe(int startX, int startY, int endX, int endY, long duration) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return null;
|
||||
Path path = new Path();
|
||||
path.moveTo(startX, startY);
|
||||
path.lineTo(endX, endY);
|
||||
GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, duration);
|
||||
return new GestureDescription.Builder().addStroke(stroke).build();
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,15 @@ public class BootstrapPlugin extends Plugin {
|
||||
private static final String BOOTSTRAP_URL_X86 =
|
||||
"https://github.com/termux/termux-packages/releases/download/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-i686.zip";
|
||||
|
||||
private static final String BOOTSTRAP_MIRROR_AARCH64 =
|
||||
"https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-aarch64.zip";
|
||||
private static final String BOOTSTRAP_MIRROR_ARM =
|
||||
"https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-arm.zip";
|
||||
private static final String BOOTSTRAP_MIRROR_X86_64 =
|
||||
"https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-x86_64.zip";
|
||||
private static final String BOOTSTRAP_MIRROR_X86 =
|
||||
"https://mirror.termux.dev/termux/termux-packages/bootstrap-2026.02.12-r1%2Bapt.android-7/bootstrap-i686.zip";
|
||||
|
||||
private static final String TERMUX_PREFIX = "/data/data/com.termux/files/usr";
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
@@ -50,7 +59,7 @@ public class BootstrapPlugin extends Plugin {
|
||||
private String stagingDir;
|
||||
private String homeDir;
|
||||
private String binDir;
|
||||
private boolean isInstalling = false;
|
||||
private volatile boolean isInstalling = false;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
@@ -108,16 +117,28 @@ public class BootstrapPlugin extends Plugin {
|
||||
|
||||
private void doInstall(PluginCall call) throws Exception {
|
||||
String arch = getArch();
|
||||
String bootstrapUrl = getBootstrapUrl(arch);
|
||||
String[] urls = getBootstrapUrls(arch);
|
||||
|
||||
sendProgress(call, "Downloading bootstrap for " + arch + "...", 0);
|
||||
|
||||
File zipFile = new File(getContext().getCacheDir(), "bootstrap.zip");
|
||||
downloadFile(bootstrapUrl, zipFile, (downloaded, total) -> {
|
||||
Exception lastError = null;
|
||||
for (String url : urls) {
|
||||
try {
|
||||
downloadFile(url, zipFile, (downloaded, total) -> {
|
||||
int percent = total > 0 ? (int)(downloaded * 100 / total) : 0;
|
||||
String sizeMB = String.format("%.1f", downloaded / (1024.0 * 1024.0));
|
||||
sendProgress(call, "Downloading... " + sizeMB + " MB (" + percent + "%)", percent / 3);
|
||||
});
|
||||
lastError = null;
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
lastError = e;
|
||||
Log.w(TAG, "Download from " + url + " failed, trying next mirror...");
|
||||
sendProgress(call, "Retrying alternate source...", 5);
|
||||
}
|
||||
}
|
||||
if (lastError != null) throw lastError;
|
||||
|
||||
sendProgress(call, "Extracting bootstrap...", 35);
|
||||
|
||||
@@ -182,9 +203,8 @@ public class BootstrapPlugin extends Plugin {
|
||||
conn.connect();
|
||||
|
||||
int total = conn.getContentLength();
|
||||
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
||||
FileOutputStream out = new FileOutputStream(outputFile);
|
||||
|
||||
try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
||||
FileOutputStream out = new FileOutputStream(outputFile)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
long downloaded = 0;
|
||||
int read;
|
||||
@@ -199,22 +219,19 @@ public class BootstrapPlugin extends Plugin {
|
||||
lastNotify = now;
|
||||
}
|
||||
}
|
||||
|
||||
out.flush();
|
||||
out.close();
|
||||
in.close();
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
|
||||
List<String[]> symlinks = new ArrayList<>();
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
|
||||
ZipInputStream zis;
|
||||
ZipEntry entry;
|
||||
int extracted = 0;
|
||||
int total = 0;
|
||||
|
||||
java.util.Enumeration<java.util.zip.ZipEntry> entries = java.util.Collections.emptyEnumeration();
|
||||
|
||||
java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile);
|
||||
total = zf.size();
|
||||
zf.close();
|
||||
@@ -240,13 +257,13 @@ public class BootstrapPlugin extends Plugin {
|
||||
} else {
|
||||
File parent = outFile.getParentFile();
|
||||
if (parent != null) parent.mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(outFile);
|
||||
try (FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
while ((len = zis.read(buf)) > 0) {
|
||||
fos.write(buf, 0, len);
|
||||
}
|
||||
fos.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
extracted++;
|
||||
@@ -558,6 +575,201 @@ public class BootstrapPlugin extends Plugin {
|
||||
notifyListeners("bootstrap-progress", event);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void installHermes(PluginCall call) {
|
||||
call.setKeepAlive(true);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String prefix = call.getString("prefix", prefixDir);
|
||||
String home = call.getString("home", homeDir + "/home");
|
||||
String hermesDir = home + "/.hermes";
|
||||
String venvDir = home + "/hermes-venv";
|
||||
|
||||
new File(hermesDir).mkdirs();
|
||||
|
||||
String hermesLink = venvDir + "/bin/hermes";
|
||||
if (new File(hermesLink).exists()) {
|
||||
call.resolve(new JSObject().put("installed", true).put("path", hermesLink).put("venv", venvDir));
|
||||
return;
|
||||
}
|
||||
|
||||
JSObject setup = setupVirtualEnvInternal(prefix, home, venvDir);
|
||||
String pipBin = setup.getString("pip");
|
||||
if (pipBin == null || !new File(pipBin).exists()) {
|
||||
call.reject("Failed to prepare internal virtual environment");
|
||||
return;
|
||||
}
|
||||
|
||||
String pipOutput = runBootstrapCommand(prefix, home,
|
||||
"\"" + pipBin + "\" install --upgrade pip setuptools wheel && " +
|
||||
"\"" + pipBin + "\" install --prefer-binary hermes-agent");
|
||||
|
||||
if (!new File(hermesLink).exists()) {
|
||||
call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(800, pipOutput.length())));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Hermes agent installed at " + venvDir + "/bin/hermes");
|
||||
call.resolve(new JSObject()
|
||||
.put("installed", true)
|
||||
.put("path", venvDir + "/bin/hermes")
|
||||
.put("venv", venvDir)
|
||||
.put("dir", hermesDir));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "installHermes failed", e);
|
||||
try { call.reject("installHermes failed: " + e.getMessage()); } catch (Exception ignored) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void hermesExec(PluginCall call) {
|
||||
call.setKeepAlive(true);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String command = call.getString("command", "");
|
||||
String home = call.getString("home", homeDir + "/home");
|
||||
String venvDir = call.getString("venv", home + "/hermes-venv");
|
||||
|
||||
if (command.isEmpty()) {
|
||||
call.reject("command required");
|
||||
return;
|
||||
}
|
||||
|
||||
String hermesBin = venvDir + "/bin/hermes";
|
||||
if (!new File(hermesBin).exists()) {
|
||||
call.reject("Hermes not installed");
|
||||
return;
|
||||
}
|
||||
|
||||
String fullCmd = hermesBin + " " + command;
|
||||
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c", fullCmd);
|
||||
pb.environment().put("HOME", home);
|
||||
pb.environment().put("PATH", venvDir + "/bin:" + prefixDir + "/bin:/system/bin");
|
||||
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
|
||||
pb.environment().put("TERMUX_HOME", home);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String output = drainProcess(p);
|
||||
int exitCode = p.waitFor();
|
||||
|
||||
call.resolve(new JSObject()
|
||||
.put("output", output)
|
||||
.put("exitCode", exitCode)
|
||||
.put("ok", exitCode == 0));
|
||||
} catch (Exception e) {
|
||||
try { call.reject("hermesExec failed: " + e.getMessage()); } catch (Exception ignored) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setupVirtualEnv(PluginCall call) {
|
||||
call.setKeepAlive(true);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String prefix = call.getString("prefix", prefixDir);
|
||||
String home = call.getString("home", homeDir + "/home");
|
||||
String venvDir = call.getString("venv", filesDir + "/venv/default");
|
||||
|
||||
call.resolve(setupVirtualEnvInternal(prefix, home, venvDir));
|
||||
} catch (Exception e) {
|
||||
try { call.reject("setupVirtualEnv failed: " + e.getMessage()); } catch (Exception ignored) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void venvPipInstall(PluginCall call) {
|
||||
call.setKeepAlive(true);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String prefix = call.getString("prefix", prefixDir);
|
||||
String home = call.getString("home", homeDir + "/home");
|
||||
String venvDir = call.getString("venv", filesDir + "/venv/default");
|
||||
String packages = call.getString("packages", "");
|
||||
if (packages.trim().isEmpty()) {
|
||||
call.reject("packages required");
|
||||
return;
|
||||
}
|
||||
|
||||
String pip = venvDir + "/bin/pip";
|
||||
if (!new File(pip).exists()) {
|
||||
call.reject("venv pip not found. Run setupVirtualEnv first.");
|
||||
return;
|
||||
}
|
||||
|
||||
String output = runBootstrapCommand(prefix, home,
|
||||
"\"" + pip + "\" install --prefer-binary " + packages);
|
||||
call.resolve(new JSObject().put("ok", true).put("output", output));
|
||||
} catch (Exception e) {
|
||||
try { call.reject("venvPipInstall failed: " + e.getMessage()); } catch (Exception ignored) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private JSObject setupVirtualEnvInternal(String prefix, String home, String venvDir) throws Exception {
|
||||
new File(venvDir).getParentFile().mkdirs();
|
||||
String pkgBin = prefix + "/bin/pkg";
|
||||
String aptBin = prefix + "/bin/apt";
|
||||
String python3 = prefix + "/bin/python3";
|
||||
|
||||
if (!new File(pkgBin).exists() && !new File(aptBin).exists()) {
|
||||
throw new RuntimeException("Bootstrap tools missing. Install internal dev environment first.");
|
||||
}
|
||||
|
||||
if (!new File(python3).exists()) {
|
||||
String installer = new File(pkgBin).exists() ? pkgBin : aptBin;
|
||||
runBootstrapCommand(prefix, home,
|
||||
"sh \"" + installer + "\" install -y python clang rust make pkg-config libffi openssl");
|
||||
}
|
||||
|
||||
if (!new File(python3).exists()) {
|
||||
throw new RuntimeException("python3 unavailable after install");
|
||||
}
|
||||
|
||||
String venvPython = venvDir + "/bin/python";
|
||||
String venvPip = venvDir + "/bin/pip";
|
||||
if (!new File(venvPip).exists()) {
|
||||
runBootstrapCommand(prefix, home,
|
||||
"\"" + python3 + "\" -m venv \"" + venvDir + "\"");
|
||||
}
|
||||
if (!new File(venvPip).exists()) {
|
||||
throw new RuntimeException("Failed to create Python venv");
|
||||
}
|
||||
|
||||
runBootstrapCommand(prefix, home,
|
||||
"\"" + venvPip + "\" install --upgrade pip setuptools wheel");
|
||||
|
||||
return new JSObject()
|
||||
.put("ok", true)
|
||||
.put("venv", venvDir)
|
||||
.put("python", venvPython)
|
||||
.put("pip", venvPip);
|
||||
}
|
||||
|
||||
private String runBootstrapCommand(String prefix, String home, String command) throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c",
|
||||
"export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " +
|
||||
"LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" " +
|
||||
"ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " + command);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String out = drainProcess(p);
|
||||
int code = p.waitFor();
|
||||
if (code != 0) throw new RuntimeException("cmd failed(" + code + "): " + out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private String drainProcess(Process p) throws Exception {
|
||||
java.io.InputStream is = p.getInputStream();
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int r;
|
||||
while ((r = is.read(buf)) > 0) baos.write(buf, 0, r);
|
||||
return baos.toString("UTF-8");
|
||||
}
|
||||
|
||||
private String getArch() {
|
||||
String abi = android.os.Build.SUPPORTED_ABIS[0];
|
||||
switch (abi) {
|
||||
@@ -569,16 +781,20 @@ public class BootstrapPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private String getBootstrapUrl(String arch) {
|
||||
private String[] getBootstrapUrls(String arch) {
|
||||
switch (arch) {
|
||||
case "aarch64": return BOOTSTRAP_URL_AARCH64;
|
||||
case "arm": return BOOTSTRAP_URL_ARM;
|
||||
case "x86_64": return BOOTSTRAP_URL_X86_64;
|
||||
case "i686": return BOOTSTRAP_URL_X86;
|
||||
default: return BOOTSTRAP_URL_AARCH64;
|
||||
case "aarch64": return new String[]{BOOTSTRAP_URL_AARCH64, BOOTSTRAP_MIRROR_AARCH64};
|
||||
case "arm": return new String[]{BOOTSTRAP_URL_ARM, BOOTSTRAP_MIRROR_ARM};
|
||||
case "x86_64": return new String[]{BOOTSTRAP_URL_X86_64, BOOTSTRAP_MIRROR_X86_64};
|
||||
case "i686": return new String[]{BOOTSTRAP_URL_X86, BOOTSTRAP_MIRROR_X86};
|
||||
default: return new String[]{BOOTSTRAP_URL_AARCH64, BOOTSTRAP_MIRROR_AARCH64};
|
||||
}
|
||||
}
|
||||
|
||||
private String getMirrorUrl(String primaryUrl) {
|
||||
return primaryUrl.replace("https://github.com/termux/", "https://mirror.termux.dev/termux/");
|
||||
}
|
||||
|
||||
private void deleteRecursive(File file) {
|
||||
if (file.isDirectory()) {
|
||||
File[] children = file.listFiles();
|
||||
|
||||
@@ -11,6 +11,7 @@ public class MainActivity extends BridgeActivity {
|
||||
registerPlugin(InstallerPlugin.class);
|
||||
registerPlugin(WakePlugin.class);
|
||||
registerPlugin(BootstrapPlugin.class);
|
||||
registerPlugin(AutoGLMPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ import com.getcapacitor.annotation.Permission;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@CapacitorPlugin(name = "Shell",
|
||||
permissions = {
|
||||
@@ -25,7 +24,7 @@ import java.util.Map;
|
||||
)
|
||||
public class ShellPlugin extends Plugin {
|
||||
private static final String TAG = "ShellPlugin";
|
||||
private final Map<String, Process> activeProcesses = new HashMap<>();
|
||||
private final Map<String, Process> activeProcesses = new ConcurrentHashMap<>();
|
||||
private String currentCwd = null;
|
||||
private String homeDir = null;
|
||||
private String toolsDir = null;
|
||||
@@ -228,9 +227,9 @@ public class ShellPlugin extends Plugin {
|
||||
File file = new File(path);
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
java.io.FileWriter writer = new java.io.FileWriter(file);
|
||||
try (java.io.FileWriter writer = new java.io.FileWriter(file)) {
|
||||
writer.write(content);
|
||||
writer.close();
|
||||
}
|
||||
call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length()));
|
||||
} catch (Exception e) {
|
||||
call.reject("Write failed: " + e.getMessage());
|
||||
@@ -250,13 +249,13 @@ public class ShellPlugin extends Plugin {
|
||||
call.reject("File not found: " + path);
|
||||
return;
|
||||
}
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
}
|
||||
call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath()));
|
||||
} catch (Exception e) {
|
||||
call.reject("Read failed: " + e.getMessage());
|
||||
@@ -367,12 +366,15 @@ public class ShellPlugin extends Plugin {
|
||||
try {
|
||||
File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION");
|
||||
if (versionFile.exists()) {
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile));
|
||||
String version = reader.readLine();
|
||||
reader.close();
|
||||
String version;
|
||||
try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile))) {
|
||||
version = reader.readLine();
|
||||
}
|
||||
return version != null ? version : "unknown";
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Unable to read Termux version", e);
|
||||
}
|
||||
return "installed";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
public class WakePlugin extends Plugin {
|
||||
private PowerManager.WakeLock screenWakeLock;
|
||||
private PowerManager.WakeLock cpuWakeLock;
|
||||
private boolean isHeld = false;
|
||||
private volatile boolean isHeld = false;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
@@ -29,11 +29,19 @@ public class WakePlugin extends Plugin {
|
||||
}
|
||||
|
||||
try {
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
|
||||
if (pm == null) {
|
||||
call.reject("Power service unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
screenWakeLock = pm.newWakeLock(
|
||||
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
|
||||
@@ -57,9 +65,13 @@ public class WakePlugin extends Plugin {
|
||||
@PluginMethod
|
||||
public void release(PluginCall call) {
|
||||
try {
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (screenWakeLock != null && screenWakeLock.isHeld()) {
|
||||
screenWakeLock.release();
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
<string name="title_activity_main">Z.AI Chat</string>
|
||||
<string name="package_name">ai.z.chat</string>
|
||||
<string name="custom_url_scheme">ai.z.chat</string>
|
||||
<string name="accessibility_service_desc">Allows Z.AI Chat to control your device — tap, swipe, type, and read the screen — for AI-powered automation in coding and agentic modes.</string>
|
||||
<string name="accessibility_service_summary">AI device control for agentic automation</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeAllMask"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds|flagIncludeNotImportantViews"
|
||||
android:canPerformGestures="true"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canTakeScreenshot="true"
|
||||
android:label="@string/app_name"
|
||||
android:notificationTimeout="100"
|
||||
android:description="@string/accessibility_service_desc"
|
||||
android:settingsActivity="ai.z.chat.MainActivity"
|
||||
android:summary="@string/accessibility_service_summary" />
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zai-chat",
|
||||
"version": "2.3.0",
|
||||
"version": "3.2.0",
|
||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
BIN
releases/Z.AI-Chat-v3.1.1-release.apk.idsig
Normal file
BIN
releases/Z.AI-Chat-v3.1.1-release.apk.idsig
Normal file
Binary file not shown.
BIN
releases/Z.AI-Chat-v3.1.2-release.apk.idsig
Normal file
BIN
releases/Z.AI-Chat-v3.1.2-release.apk.idsig
Normal file
Binary file not shown.
@@ -69,24 +69,24 @@
|
||||
<div id="chat-screen" class="screen">
|
||||
<div class="chat-header">
|
||||
<div class="header-left">
|
||||
<button id="menu-btn" class="icon-btn">☰</button>
|
||||
<button id="menu-btn" class="icon-btn" aria-label="Open sidebar menu">☰</button>
|
||||
<div class="header-title">
|
||||
<h2 id="conversation-title">New Chat</h2>
|
||||
<span id="current-mode-label" class="mode-label">Chat</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="file-tree-btn" class="icon-btn" title="Project Files" style="display:none">📁</button>
|
||||
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">☾</button>
|
||||
<button id="new-chat-btn" class="icon-btn" title="New chat">+</button>
|
||||
<button id="settings-btn" class="icon-btn" title="Settings">⚙</button>
|
||||
<button id="file-tree-btn" class="icon-btn" title="Project Files" aria-label="Open project files" style="display:none">📁</button>
|
||||
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme" aria-label="Toggle theme">☾</button>
|
||||
<button id="new-chat-btn" class="icon-btn" title="New chat" aria-label="Start new chat">+</button>
|
||||
<button id="settings-btn" class="icon-btn" title="Settings" aria-label="Open settings">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>Conversations</h3>
|
||||
<button id="sidebar-close" class="icon-btn">×</button>
|
||||
<button id="sidebar-close" class="icon-btn" aria-label="Close sidebar">×</button>
|
||||
</div>
|
||||
<div id="conversation-list" class="conversation-list"></div>
|
||||
<div class="sidebar-footer">
|
||||
@@ -98,7 +98,7 @@
|
||||
<div id="file-tree-panel" class="file-tree-panel">
|
||||
<div class="file-tree-header">
|
||||
<h3>Project Files</h3>
|
||||
<button id="file-tree-close" class="icon-btn">×</button>
|
||||
<button id="file-tree-close" class="icon-btn" aria-label="Close file tree">×</button>
|
||||
</div>
|
||||
<div id="file-tree-body" class="file-tree-body">
|
||||
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="file-viewer-actions">
|
||||
<button id="file-viewer-edit" class="fv-btn">Edit</button>
|
||||
<button id="file-viewer-save" class="fv-btn fv-btn-save" style="display:none">Save</button>
|
||||
<button id="file-viewer-close" class="icon-btn">×</button>
|
||||
<button id="file-viewer-close" class="icon-btn" aria-label="Close file viewer">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="file-viewer-body" class="file-viewer-body">
|
||||
@@ -153,10 +153,10 @@
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<textarea id="message-input" placeholder="Type your message..." rows="1"></textarea>
|
||||
<button id="send-btn" class="send-btn" disabled>
|
||||
<button id="send-btn" class="send-btn" aria-label="Send message" disabled>
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button id="stop-btn" class="stop-btn" style="display:none">
|
||||
<button id="stop-btn" class="stop-btn" aria-label="Stop generation" style="display:none">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="6" y="6" width="12" height="12" fill="currentColor" rx="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -166,11 +166,11 @@
|
||||
<div id="terminal-screen" class="screen">
|
||||
<div class="term-screen-container">
|
||||
<div class="term-screen-header">
|
||||
<button id="term-back-btn" class="icon-btn">←</button>
|
||||
<button id="term-back-btn" class="icon-btn" aria-label="Back to chat">←</button>
|
||||
<h2>Terminal</h2>
|
||||
<div class="term-screen-header-right">
|
||||
<span id="term-cwd-display" class="term-cwd-display">~</span>
|
||||
<button id="term-setup-tools-btn" class="icon-btn" title="Setup Dev Tools">🛠</button>
|
||||
<button id="term-setup-tools-btn" class="icon-btn" title="Setup Dev Tools" aria-label="Setup development tools">🛠</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="term-output" class="term-output"></div>
|
||||
@@ -186,8 +186,8 @@
|
||||
<div class="term-input-row">
|
||||
<span class="term-prompt">$</span>
|
||||
<input type="text" id="term-input" class="term-input" placeholder="Enter command..." autocomplete="off" spellcheck="false">
|
||||
<button id="term-run-btn" class="term-run-btn">▶</button>
|
||||
<button id="term-stop-btn" class="term-stop-btn" style="display:none">■</button>
|
||||
<button id="term-run-btn" class="term-run-btn" aria-label="Run terminal command">▶</button>
|
||||
<button id="term-stop-btn" class="term-stop-btn" aria-label="Stop terminal command" style="display:none">■</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,6 +216,17 @@
|
||||
<span class="btn-text">Install Dev Tools</span>
|
||||
<span class="btn-loader" style="display:none"></span>
|
||||
</button>
|
||||
<div style="margin-top:20px; border-top:1px solid var(--border); padding-top:16px;">
|
||||
<h3 style="margin-bottom:8px;">AutoGLM Device Control</h3>
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin-bottom:8px;">Enable accessibility service to allow AI to control your device (tap, swipe, type, read screen).</p>
|
||||
<div id="autoglm-status" style="font-size:13px; margin-bottom:8px;"></div>
|
||||
<button id="autoglm-enable-btn" class="btn-secondary" style="width:100%">Enable Device Control</button>
|
||||
</div>
|
||||
<div style="margin-top:16px; border-top:1px solid var(--border); padding-top:16px;">
|
||||
<h3 style="margin-bottom:8px;">Hermes Agent</h3>
|
||||
<p style="font-size:13px; color:var(--text-secondary); margin-bottom:8px;">Install Hermes agent for advanced AI capabilities: web search, terminal, skills, memory, browser automation.</p>
|
||||
<button id="hermes-install-btn" class="btn-secondary" style="width:100%">Install Hermes</button>
|
||||
</div>
|
||||
<button id="devsetup-back-btn" class="btn-secondary" style="margin-top:12px">Back to Terminal</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +234,7 @@
|
||||
<div id="settings-screen" class="screen">
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<button id="settings-back" class="icon-btn">←</button>
|
||||
<button id="settings-back" class="icon-btn" aria-label="Back to chat">←</button>
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
@@ -327,13 +338,38 @@
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>About</h3>
|
||||
<p class="about-text">Z.AI Chat v2.3.0</p>
|
||||
<p class="about-text">Z.AI Chat v3.2.0</p>
|
||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||
<p class="about-text">Compatible with Android 15/16</p>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Changelog</h3>
|
||||
<ul class="changelog-list">
|
||||
<li>
|
||||
<span class="changelog-version">v3.2.0</span>
|
||||
<span class="changelog-date">2026-05-20</span>
|
||||
<ul>
|
||||
<li><strong>Full In-App Virtual Env</strong> — new internal virtual environment setup with no external Termux app dependency</li>
|
||||
<li><strong>Module Installer</strong> — new AI actions: <code>[VENV_SETUP]</code> and <code>[VENV_PIP_INSTALL package]</code></li>
|
||||
<li><strong>QA Critical Fix</strong> — fixed invalid JS file check that broke PRoot install fallback</li>
|
||||
<li><strong>Stability</strong> — thread-safe process map in ShellPlugin and safer wake lock handling</li>
|
||||
<li><strong>Resource Safety</strong> — fixed high-risk stream/file descriptor leaks in bootstrap extraction and downloads</li>
|
||||
<li><strong>Accessibility</strong> — improved icon button labels for better screen reader support</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="changelog-version">v3.1.0</span>
|
||||
<span class="changelog-date">2026-05-20</span>
|
||||
<ul>
|
||||
<li><strong>AutoGLM Device Control</strong> — AccessibilityService for tap, swipe, type, long press, screenshot, UI tree, click by text/ID</li>
|
||||
<li><strong>Navigation Controls</strong> — press Back, Home, Recents, Notifications, Quick Settings, Power Dialog</li>
|
||||
<li><strong>App Control</strong> — launch apps, get current app, read full UI hierarchy</li>
|
||||
<li><strong>Hermes Agent</strong> — install hermes-agent in Python venv, execute Hermes commands from chat</li>
|
||||
<li><strong>Device Tools in AI</strong> — [DEVICE_*] and [HERMES_*] tags parsed in coding + agentic modes</li>
|
||||
<li><strong>Accessibility Setup</strong> — one-tap enable in Dev Setup screen with status indicator</li>
|
||||
<li><strong>v2 Signing</strong> — APK signed with v2 scheme (required for targetSdk 36)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="changelog-version">v2.3.0</span>
|
||||
<span class="changelog-date">2026-05-20</span>
|
||||
|
||||
321
www/js/app.js
321
www/js/app.js
@@ -6,9 +6,9 @@
|
||||
var STORAGE_KEY = 'zai_chat_';
|
||||
var MODE_PROMPTS = {
|
||||
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
|
||||
coding: 'You are an expert coding assistant. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.',
|
||||
coding: 'You are an expert coding assistant with internal tool access. Use action tags when execution is needed: [CREATE_FILE ...][/CREATE_FILE], [RUN_COMMAND]...[/RUN_COMMAND], [BUILD_APK project], [INSTALL_APK path], [VENV_SETUP], [VENV_PIP_INSTALL package]. Keep responses concise and complete.',
|
||||
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
|
||||
agentic: 'You are an autonomous coding agent with FULL control of an Android device. You have a real terminal with shell access, can read/write any file, build APKs, install apps, and execute any command.\n\n## Your Tools (use these EXACT formats):\n\n### Write a file:\n[CREATE_FILE path/to/file.ext]\nfile contents here\n[/CREATE_FILE]\n\n### Run a shell command:\n[RUN_COMMAND]\ncommand here\n[/RUN_COMMAND]\n\n### Build Android APK:\n[BUILD_APK project_name]\n\n### Install APK on device:\n[INSTALL_APK /path/to/file.apk]\n\n### List files:\n[RUN_COMMAND]\nfind . -type f | head -50\n[/RUN_COMMAND]\n\n## IMPORTANT RULES:\n1. ALWAYS use [CREATE_FILE] for EVERY file — the system auto-saves them to the device\n2. ALWAYS use [BUILD_APK] after writing files — the system auto-compiles\n3. ALWAYS use [INSTALL_APK] after building — the system auto-installs\n4. NEVER say "I installed it" unless you used [INSTALL_APK] — the system executes your tags automatically\n5. If a build fails, you will see the error output — FIX the code and try again\n6. Generate COMPLETE files — never use "// ... existing code ..."\n7. For Java: use package ai.z.app, target SDK 36, compile SDK 36\n8. You can run ANY shell command: ls, cat, mkdir, chmod, cp, grep, find, etc.\n9. If Termux tools are available, use: aapt2, d8, ecj, javac, apksigner\n10. Write ALL files first, THEN build, THEN install. Always in that order.\n11. When the user asks to build an app, generate EVERY file needed for a complete working app.\n12. CRITICAL: When you have FULLY completed the user\'s entire task (all files written, all builds done, all installs done), output [TASK_COMPLETE] on a line by itself. This is MANDATORY — the system uses it to know you are done.\n13. If your response is cut off or you haven\'t finished all work, do NOT output [TASK_COMPLETE]. The system will automatically continue you.\n14. NEVER output [TASK_COMPLETE] unless the ENTIRE task is truly done. If there are more files to write, commands to run, or builds to perform, keep working.'
|
||||
agentic: 'You are an autonomous agent with full control of this app sandbox: internal Linux bootstrap, terminal, virtual env, build tools, and device UI via AutoGLM.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell:\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\n\n## In-App Virtual Env:\n[VENV_SETUP]\n[VENV_PIP_INSTALL package_name]\n\n## AutoGLM Device Control:\n[DEVICE_TAP x y]\n[DEVICE_LONG_PRESS x y]\n[DEVICE_SWIPE startX startY endX endY]\n[DEVICE_TYPE text]\n[DEVICE_PRESS_BACK]\n[DEVICE_PRESS_HOME]\n[DEVICE_PRESS_RECENTS]\n[DEVICE_SCREENSHOT]\n[DEVICE_UI_TREE]\n[DEVICE_CLICK_TEXT button text]\n[DEVICE_CLICK_ID com.example:id/viewId]\n[DEVICE_LAUNCH com.example.app]\n[DEVICE_CURRENT_APP]\n\n## Hermes Agent:\n[HERMES_INSTALL]\n[HERMES_EXEC command]\n\n## Rules:\n1. Prefer internal sandbox tools and virtual env first\n2. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n3. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n4. Generate complete files, never stubs\n5. For Java: package ai.z.app, target SDK 36\n6. Output [TASK_COMPLETE] only when all work is done'
|
||||
};
|
||||
|
||||
var BUILD_SCRIPT = [
|
||||
@@ -623,7 +623,7 @@
|
||||
if (state.streamingContent) {
|
||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||
}
|
||||
var retryDiv = appendRetryMessage(err, requestBody, conv);
|
||||
appendRetryMessage(err, requestBody, conv);
|
||||
} else if (state.streamingContent) {
|
||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||
}
|
||||
@@ -1175,6 +1175,9 @@
|
||||
hasProot: false,
|
||||
prootPath: '',
|
||||
nativeLibDir: '',
|
||||
hermesPath: '',
|
||||
hermesVenv: '',
|
||||
autoglmEnabled: false,
|
||||
commandQueue: []
|
||||
};
|
||||
|
||||
@@ -1234,7 +1237,7 @@
|
||||
async function installApk(path) {
|
||||
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
|
||||
try {
|
||||
var result = await Installer.installApk({ path: path });
|
||||
await Installer.installApk({ path: path });
|
||||
termPrint('[APK install triggered: ' + path + ']', 'success');
|
||||
} catch(e) {
|
||||
termPrint('[Install failed: ' + e.message + ']', 'err');
|
||||
@@ -1423,6 +1426,23 @@
|
||||
var runCmdRegex = /\[RUN_COMMAND\]\n([\s\S]*?)\[\/RUN_COMMAND\]/gi;
|
||||
var buildApkRegex = /\[BUILD_APK\s+([^\]]+)\]/gi;
|
||||
var installApkRegex = /\[INSTALL_APK\s+([^\]]+)\]/gi;
|
||||
var deviceTapRegex = /\[DEVICE_TAP\s+(\d+)\s+(\d+)\]/gi;
|
||||
var deviceLongPressRegex = /\[DEVICE_LONG_PRESS\s+(\d+)\s+(\d+)\]/gi;
|
||||
var deviceSwipeRegex = /\[DEVICE_SWIPE\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\]/gi;
|
||||
var deviceTypeRegex = /\[DEVICE_TYPE\s+([^\]]+)\]/gi;
|
||||
var devicePressBackRegex = /\[DEVICE_PRESS_BACK\]/gi;
|
||||
var devicePressHomeRegex = /\[DEVICE_PRESS_HOME\]/gi;
|
||||
var devicePressRecentsRegex = /\[DEVICE_PRESS_RECENTS\]/gi;
|
||||
var deviceScreenshotRegex = /\[DEVICE_SCREENSHOT\]/gi;
|
||||
var deviceUiTreeRegex = /\[DEVICE_UI_TREE\]/gi;
|
||||
var deviceClickTextRegex = /\[DEVICE_CLICK_TEXT\s+([^\]]+)\]/gi;
|
||||
var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi;
|
||||
var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi;
|
||||
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
|
||||
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
|
||||
var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/gi;
|
||||
var venvSetupRegex = /\[VENV_SETUP\]/gi;
|
||||
var venvPipInstallRegex = /\[VENV_PIP_INSTALL\s+([^\]]+)\]/gi;
|
||||
var codeBlockFileRegex = /```(\w+)\s*\n([\s\S]*?)```/gi;
|
||||
var match;
|
||||
|
||||
@@ -1438,6 +1458,57 @@
|
||||
while ((match = installApkRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'install_apk', path: match[1].trim() });
|
||||
}
|
||||
while ((match = deviceTapRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_tap', x: parseInt(match[1]), y: parseInt(match[2]) });
|
||||
}
|
||||
while ((match = deviceLongPressRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_long_press', x: parseInt(match[1]), y: parseInt(match[2]) });
|
||||
}
|
||||
while ((match = deviceSwipeRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_swipe', startX: parseInt(match[1]), startY: parseInt(match[2]), endX: parseInt(match[3]), endY: parseInt(match[4]) });
|
||||
}
|
||||
while ((match = deviceTypeRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_type', text: match[1].trim() });
|
||||
}
|
||||
while ((match = devicePressBackRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_press_back' });
|
||||
}
|
||||
while ((match = devicePressHomeRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_press_home' });
|
||||
}
|
||||
while ((match = devicePressRecentsRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_press_recents' });
|
||||
}
|
||||
while ((match = deviceScreenshotRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_screenshot' });
|
||||
}
|
||||
while ((match = deviceUiTreeRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_ui_tree' });
|
||||
}
|
||||
while ((match = deviceClickTextRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_click_text', text: match[1].trim() });
|
||||
}
|
||||
while ((match = deviceClickIdRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_click_id', viewId: match[1].trim() });
|
||||
}
|
||||
while ((match = deviceLaunchRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_launch', pkg: match[1].trim() });
|
||||
}
|
||||
while ((match = deviceCurrentAppRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'device_current_app' });
|
||||
}
|
||||
while ((match = hermesInstallRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'hermes_install' });
|
||||
}
|
||||
while ((match = hermesExecRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'hermes_exec', command: match[1].trim() });
|
||||
}
|
||||
while ((match = venvSetupRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'venv_setup' });
|
||||
}
|
||||
while ((match = venvPipInstallRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'venv_pip_install', packages: match[1].trim() });
|
||||
}
|
||||
while ((match = codeBlockFileRegex.exec(content)) !== null) {
|
||||
var lang = match[1];
|
||||
var code = match[2];
|
||||
@@ -1457,6 +1528,8 @@
|
||||
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
|
||||
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
|
||||
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
|
||||
var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
|
||||
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
|
||||
|
||||
var actionBar = document.createElement('div');
|
||||
actionBar.className = 'msg-actions';
|
||||
@@ -1606,7 +1679,10 @@
|
||||
var hasAction = content.indexOf('[CREATE_FILE') >= 0 ||
|
||||
content.indexOf('[RUN_COMMAND]') >= 0 ||
|
||||
content.indexOf('[BUILD_APK') >= 0 ||
|
||||
content.indexOf('[INSTALL_APK') >= 0;
|
||||
content.indexOf('[INSTALL_APK') >= 0 ||
|
||||
content.indexOf('[DEVICE_') >= 0 ||
|
||||
content.indexOf('[HERMES_') >= 0 ||
|
||||
content.indexOf('[VENV_') >= 0;
|
||||
var hasCodeBlock = content.indexOf('```') >= 0;
|
||||
if (!hasCodeBlock && !hasAction && content.length < 300) return true;
|
||||
if ((content.match(/```/g) || []).length % 2 !== 0) return false;
|
||||
@@ -1828,7 +1904,7 @@
|
||||
var saveBtn = $('#file-viewer-save');
|
||||
var editBtn = $('#file-viewer-edit');
|
||||
|
||||
if (!viewer) return;
|
||||
if (!viewer || !nameEl || !langEl || !contentEl || !textareaEl || !bodyEl || !editorEl || !saveBtn || !editBtn) return;
|
||||
nameEl.textContent = file.path;
|
||||
langEl.textContent = file.language;
|
||||
contentEl.textContent = file.content;
|
||||
@@ -1985,17 +2061,19 @@
|
||||
|
||||
var d8Test = await shellExec('test -f "' + jarsDir + '/d8.jar"', termState.homeDir, false);
|
||||
if (d8Test.exitCode !== 0) {
|
||||
termPrint('[*] Downloading d8.jar (DEX compiler, ~18MB)...', 'info');
|
||||
termPrint('[*] Extracting d8.jar from APK assets...', 'info');
|
||||
try {
|
||||
var dlResult = await Bootstrap.downloadFile({url: 'https://dl.google.com/android/repository/build-tools_r36-linux.zip', dest: toolsDir + '/build-tools.zip'});
|
||||
termPrint('[*] Extracting d8.jar from build-tools...', 'info');
|
||||
await shellExec('cd "' + toolsDir + '" && unzip -o build-tools.zip "*/lib/d8.jar" 2>&1 && mv */lib/d8.jar jars/d8.jar && rm -rf build-tools.zip android-*', termState.homeDir, false);
|
||||
var d8Extract = await Bootstrap.extractAsset({src: 'jars/d8.jar', dest: jarsDir + '/d8.jar'});
|
||||
termPrint('[OK] d8.jar extracted (' + Math.round(d8Extract.size/1024/1024) + ' MB)', 'success');
|
||||
} catch(e) {
|
||||
termPrint('[!] d8.jar download failed: ' + e.message, 'warning');
|
||||
termPrint('[*] Trying Termux dx package...', 'info');
|
||||
termPrint('[!] d8.jar extract failed: ' + e.message, 'warning');
|
||||
termPrint('[*] Downloading d8.jar (~18MB)...', 'info');
|
||||
try {
|
||||
var dxResult = await Bootstrap.installProot();
|
||||
} catch(e2) {}
|
||||
await Bootstrap.downloadFile({url: 'https://dl.google.com/android/repository/build-tools_r36-linux.zip', dest: toolsDir + '/build-tools.zip'});
|
||||
await shellExec('cd "' + toolsDir + '" && unzip -o build-tools.zip "*/lib/d8.jar" 2>&1 && mv */lib/d8.jar jars/d8.jar && rm -rf build-tools.zip android-*', termState.homeDir, false);
|
||||
} catch(e2) {
|
||||
termPrint('[!] d8.jar download also failed: ' + e2.message, 'err');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2082,8 +2160,9 @@
|
||||
}
|
||||
|
||||
async function tryProotExec(prootCmd, prefixUsr, pkgBin, aptBin) {
|
||||
var hasPkg = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false);
|
||||
var pkgCmd = 'sh /usr/bin/pkg update -y 2>&1 && sh /usr/bin/pkg install -y aapt2 ecj dx apksigner 2>&1';
|
||||
if (!new File(pkgBin).exists) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1';
|
||||
if (hasPkg.exitCode !== 0) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1';
|
||||
var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\'';
|
||||
|
||||
termPrint('[*] Running pkg via PRoot...', 'info');
|
||||
@@ -2117,7 +2196,7 @@
|
||||
|
||||
termPrint('[OK] Termux detected! Sending install command...', 'success');
|
||||
try {
|
||||
var runResult = await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'});
|
||||
await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'});
|
||||
termPrint('[*] Command sent to Termux. Waiting...', 'info');
|
||||
await new Promise(function(r) { setTimeout(r, 15000); });
|
||||
if (await toolsReady()) return true;
|
||||
@@ -2129,6 +2208,24 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkAutoGLMStatus() {
|
||||
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
|
||||
var statusEl = $('#autoglm-status');
|
||||
var btn = $('#autoglm-enable-btn');
|
||||
if (!AutoGLM || !statusEl) return;
|
||||
try {
|
||||
var result = await AutoGLM.isEnabled();
|
||||
if (result.enabled) {
|
||||
statusEl.innerHTML = '<span style="color:var(--success)">Device control enabled</span>';
|
||||
if (btn) { btn.textContent = 'Device Control Active'; btn.disabled = true; }
|
||||
} else {
|
||||
statusEl.innerHTML = '<span style="color:var(--warning)">Not enabled — tap to open Settings</span>';
|
||||
}
|
||||
} catch(e) {
|
||||
statusEl.innerHTML = '<span style="color:var(--text-secondary)">Checking status...</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDevEnvironment() {
|
||||
if (state.currentMode !== 'coding' && state.currentMode !== 'agentic') return;
|
||||
|
||||
@@ -2225,8 +2322,11 @@
|
||||
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
|
||||
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
|
||||
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
|
||||
var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
|
||||
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
|
||||
var hasVenv = actions.some(function(a) { return a.type && a.type.indexOf('venv_') === 0; });
|
||||
|
||||
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands) return;
|
||||
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv) return;
|
||||
|
||||
_agenticRetryCount = 0;
|
||||
var resultLog = [];
|
||||
@@ -2257,6 +2357,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDevice) {
|
||||
for (var d = 0; d < actions.length; d++) {
|
||||
var act = actions[d];
|
||||
if (act.type.indexOf('device_') !== 0) continue;
|
||||
try {
|
||||
var devResult = await executeDeviceAction(act);
|
||||
resultLog.push(devResult);
|
||||
termPrint(devResult, '');
|
||||
} catch(e) {
|
||||
resultLog.push('DEVICE_ERROR: ' + e.message);
|
||||
termPrint('[!] Device: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHermes) {
|
||||
for (var h = 0; h < actions.length; h++) {
|
||||
var hAct = actions[h];
|
||||
if (hAct.type.indexOf('hermes_') !== 0) continue;
|
||||
try {
|
||||
var hermesResult = await executeHermesAction(hAct);
|
||||
resultLog.push(hermesResult);
|
||||
termPrint(hermesResult, '');
|
||||
} catch(e) {
|
||||
resultLog.push('HERMES_ERROR: ' + e.message);
|
||||
termPrint('[!] Hermes: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasVenv) {
|
||||
for (var v = 0; v < actions.length; v++) {
|
||||
var vAct = actions[v];
|
||||
if (vAct.type.indexOf('venv_') !== 0) continue;
|
||||
try {
|
||||
var venvResult = await executeVirtualEnvAction(vAct);
|
||||
resultLog.push(venvResult);
|
||||
termPrint(venvResult, '');
|
||||
} catch(e) {
|
||||
resultLog.push('VENV_ERROR: ' + e.message);
|
||||
termPrint('[!] Venv: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBuild) {
|
||||
showStatusToast('Building APK...', 'info');
|
||||
var toolsReady = await ensureBuildTools();
|
||||
@@ -2285,6 +2430,113 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function executeDeviceAction(action) {
|
||||
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
|
||||
if (!AutoGLM) throw new Error('AutoGLM plugin not available');
|
||||
|
||||
switch (action.type) {
|
||||
case 'device_tap':
|
||||
await AutoGLM.tap({ x: action.x, y: action.y });
|
||||
return '[DEVICE] Tap(' + action.x + ', ' + action.y + ')';
|
||||
case 'device_long_press':
|
||||
await AutoGLM.longPress({ x: action.x, y: action.y });
|
||||
return '[DEVICE] LongPress(' + action.x + ', ' + action.y + ')';
|
||||
case 'device_swipe':
|
||||
await AutoGLM.swipe({ startX: action.startX, startY: action.startY, endX: action.endX, endY: action.endY });
|
||||
return '[DEVICE] Swipe(' + action.startX + ',' + action.startY + ' -> ' + action.endX + ',' + action.endY + ')';
|
||||
case 'device_type':
|
||||
await AutoGLM.typeText({ text: action.text });
|
||||
return '[DEVICE] Type: "' + action.text + '"';
|
||||
case 'device_press_back':
|
||||
await AutoGLM.pressBack();
|
||||
return '[DEVICE] Press Back';
|
||||
case 'device_press_home':
|
||||
await AutoGLM.pressHome();
|
||||
return '[DEVICE] Press Home';
|
||||
case 'device_press_recents':
|
||||
await AutoGLM.pressRecents();
|
||||
return '[DEVICE] Press Recents';
|
||||
case 'device_screenshot':
|
||||
var ssResult = await AutoGLM.takeScreenshot({});
|
||||
return '[DEVICE] Screenshot: ' + (ssResult.path || 'saved');
|
||||
case 'device_ui_tree':
|
||||
var treeResult = await AutoGLM.getUITree({});
|
||||
var tree = treeResult.tree || '{}';
|
||||
return '[DEVICE] UI Tree: ' + tree.substring(0, 2000);
|
||||
case 'device_click_text':
|
||||
var clickResult = await AutoGLM.clickByText({ text: action.text });
|
||||
return '[DEVICE] Click "' + action.text + '": ' + (clickResult.ok ? 'OK' : 'not found');
|
||||
case 'device_click_id':
|
||||
var idResult = await AutoGLM.clickNode({ viewId: action.viewId });
|
||||
return '[DEVICE] Click ID "' + action.viewId + '": ' + (idResult.ok ? 'OK' : 'not found');
|
||||
case 'device_launch':
|
||||
await AutoGLM.launchApp({ package: action.pkg });
|
||||
return '[DEVICE] Launch: ' + action.pkg;
|
||||
case 'device_current_app':
|
||||
var appResult = await AutoGLM.getCurrentApp({});
|
||||
return '[DEVICE] Current app: ' + (appResult.package || 'unknown');
|
||||
default:
|
||||
return '[DEVICE] Unknown action: ' + action.type;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeHermesAction(action) {
|
||||
var Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
|
||||
if (!Bootstrap) throw new Error('Bootstrap plugin not available');
|
||||
|
||||
switch (action.type) {
|
||||
case 'hermes_install':
|
||||
showStatusToast('Installing Hermes agent...', 'info');
|
||||
var installResult = await Bootstrap.installHermes({});
|
||||
termState.hermesPath = installResult.path;
|
||||
termState.hermesVenv = installResult.venv;
|
||||
return '[HERMES] Installed: ' + installResult.path;
|
||||
case 'hermes_exec':
|
||||
if (!termState.hermesVenv) {
|
||||
try {
|
||||
var status = await Bootstrap.installHermes({});
|
||||
termState.hermesPath = status.path;
|
||||
termState.hermesVenv = status.venv;
|
||||
} catch(e) {
|
||||
throw new Error('Hermes not installed: ' + e.message);
|
||||
}
|
||||
}
|
||||
showStatusToast('Hermes: ' + action.command.substring(0, 30), 'info');
|
||||
var execResult = await Bootstrap.hermesExec({ command: action.command, venv: termState.hermesVenv });
|
||||
var hermesOutput = execResult.output || '';
|
||||
if (hermesOutput.length > 3000) hermesOutput = hermesOutput.substring(0, 1500) + '\n...truncated...\n' + hermesOutput.substring(hermesOutput.length - 1000);
|
||||
return '[HERMES] ' + action.command + '\n' + hermesOutput;
|
||||
default:
|
||||
return '[HERMES] Unknown action: ' + action.type;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeVirtualEnvAction(action) {
|
||||
var bsPlugin = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
|
||||
if (!bsPlugin) throw new Error('Bootstrap plugin not available');
|
||||
var venvPath = termState.homeDir ? (termState.homeDir.replace(/\/home$/, '') + '/venv/default') : '';
|
||||
|
||||
switch (action.type) {
|
||||
case 'venv_setup': {
|
||||
showStatusToast('Setting up in-app virtual environment...', 'info');
|
||||
var setup = await bsPlugin.setupVirtualEnv({ venv: venvPath });
|
||||
termState.venvPath = setup.venv;
|
||||
return '[VENV] Ready: ' + setup.venv;
|
||||
}
|
||||
case 'venv_pip_install': {
|
||||
if (!termState.venvPath) {
|
||||
var init = await bsPlugin.setupVirtualEnv({ venv: venvPath });
|
||||
termState.venvPath = init.venv;
|
||||
}
|
||||
showStatusToast('Installing module(s): ' + action.packages, 'info');
|
||||
var installed = await bsPlugin.venvPipInstall({ venv: termState.venvPath, packages: action.packages });
|
||||
return '[VENV] pip install ' + action.packages + '\n' + (installed.output || '').substring(0, 1200);
|
||||
}
|
||||
default:
|
||||
return '[VENV] Unknown action: ' + action.type;
|
||||
}
|
||||
}
|
||||
|
||||
async function autoDeployFile(action) {
|
||||
var path = action.path;
|
||||
if (!path.startsWith('/')) {
|
||||
@@ -2516,7 +2768,7 @@
|
||||
});
|
||||
|
||||
try {
|
||||
var result = await Bootstrap.install();
|
||||
await Bootstrap.install();
|
||||
|
||||
progressText.textContent = 'Fixing file permissions...';
|
||||
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||
@@ -2644,6 +2896,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
var autoglmBtn = $('#autoglm-enable-btn');
|
||||
if (autoglmBtn) {
|
||||
checkAutoGLMStatus();
|
||||
autoglmBtn.addEventListener('click', function() {
|
||||
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
|
||||
if (AutoGLM) {
|
||||
AutoGLM.openSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var hermesBtn = $('#hermes-install-btn');
|
||||
if (hermesBtn) {
|
||||
hermesBtn.addEventListener('click', async function() {
|
||||
var Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
|
||||
if (!Bootstrap) return;
|
||||
hermesBtn.disabled = true;
|
||||
hermesBtn.textContent = 'Installing Hermes...';
|
||||
try {
|
||||
var result = await Bootstrap.installHermes({});
|
||||
hermesBtn.textContent = 'Hermes Installed!';
|
||||
hermesBtn.disabled = true;
|
||||
termState.hermesPath = result.path;
|
||||
termState.hermesVenv = result.venv;
|
||||
termPrint('[OK] Hermes installed: ' + result.path, 'success');
|
||||
} catch(e) {
|
||||
hermesBtn.textContent = 'Install Failed - Retry';
|
||||
hermesBtn.disabled = false;
|
||||
termPrint('[!] Hermes install failed: ' + e.message, 'err');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$$('.term-quick-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var cmd = this.dataset.cmd;
|
||||
|
||||
Reference in New Issue
Block a user