Compare commits
4 Commits
18
README.md
18
README.md
@@ -631,6 +631,24 @@ data: [DONE]
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v3.3.0 (2026-05-21)
|
||||||
|
- **File Manager** — browse device files (App Files, Downloads, Camera, Storage), open/preview any file, install APKs directly
|
||||||
|
- **SSH / Remote Access** — AI can SSH into external machines, SCP upload/download, curl URLs
|
||||||
|
- **Approval Gate** — all sensitive actions (SSH, SCP, curl, adb, pip, npm, etc.) require explicit user approval via in-app dialog
|
||||||
|
- **New Action Tags** — `[SSH_EXEC]`, `[SSH_UPLOAD]`, `[SSH_DOWNLOAD]`, `[REMOTE_EXEC]`, `[CURL_EXEC]`
|
||||||
|
- **Agentic Mode** — updated system prompt with full external access documentation
|
||||||
|
|
||||||
|
### 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)
|
### v3.1.0 (2026-05-20)
|
||||||
- **AutoGLM Device Control** — `AccessibilityService` for full device automation: tap, swipe, long press, type text, screenshot, UI tree
|
- **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
|
- **Navigation Controls** — press Back, Home, Recents, Notifications, Quick Settings, Power Dialog via global actions
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
apply plugin: 'com.android.application'
|
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 {
|
android {
|
||||||
namespace = "ai.z.chat"
|
namespace = "ai.z.chat"
|
||||||
compileSdk = rootProject.ext.compileSdkVersion
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
@@ -7,8 +21,8 @@ android {
|
|||||||
applicationId "ai.z.chat"
|
applicationId "ai.z.chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 21
|
versionCode 25
|
||||||
versionName "3.1.1"
|
versionName "3.3.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
@@ -18,15 +32,15 @@ android {
|
|||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
storeFile file('debug.keystore')
|
storeFile file('debug.keystore')
|
||||||
storePassword 'android'
|
storePassword debugStorePass
|
||||||
keyAlias 'androiddebugkey'
|
keyAlias debugKeyAlias
|
||||||
keyPassword 'android'
|
keyPassword debugKeyPass
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
storeFile file('release.keystore')
|
storeFile file('release.keystore')
|
||||||
storePassword 'zaichat'
|
storePassword releaseStorePass
|
||||||
keyAlias 'zai-chat'
|
keyAlias releaseKeyAlias
|
||||||
keyPassword 'zaichat'
|
keyPassword releaseKeyPass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.getcapacitor.annotation.Permission;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
@CapacitorPlugin(
|
@CapacitorPlugin(
|
||||||
name = "AutoGLM",
|
name = "AutoGLM",
|
||||||
@@ -130,7 +129,11 @@ public class AutoGLMPlugin extends Plugin {
|
|||||||
destPath = getContext().getCacheDir() + "/autoglm_screenshot.png";
|
destPath = getContext().getCacheDir() + "/autoglm_screenshot.png";
|
||||||
}
|
}
|
||||||
svc.takeScreenshot(destPath);
|
svc.takeScreenshot(destPath);
|
||||||
try { Thread.sleep(500); } catch (Exception e) {}
|
try {
|
||||||
|
Thread.sleep(500);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
call.resolve(new JSObject().put("path", destPath).put("ok", true));
|
call.resolve(new JSObject().put("path", destPath).put("ok", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.util.List;
|
|||||||
public class AutoGLMService extends AccessibilityService {
|
public class AutoGLMService extends AccessibilityService {
|
||||||
|
|
||||||
private static final String TAG = "AutoGLMService";
|
private static final String TAG = "AutoGLMService";
|
||||||
private static AutoGLMService instance;
|
private static volatile AutoGLMService instance;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
@@ -138,12 +138,14 @@ public class AutoGLMService extends AccessibilityService {
|
|||||||
screenshot.getHardwareBuffer(), screenshot.getColorSpace());
|
screenshot.getHardwareBuffer(), screenshot.getColorSpace());
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
Bitmap softwareBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
Bitmap softwareBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||||
FileOutputStream fos = new FileOutputStream(destPath);
|
try (FileOutputStream fos = new FileOutputStream(destPath)) {
|
||||||
softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
|
softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
|
||||||
fos.close();
|
}
|
||||||
softwareBitmap.recycle();
|
softwareBitmap.recycle();
|
||||||
}
|
}
|
||||||
screenshot.getHardwareBuffer().close();
|
if (screenshot.getHardwareBuffer() != null) {
|
||||||
|
screenshot.getHardwareBuffer().close();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Screenshot save failed", e);
|
Log.e(TAG, "Screenshot save failed", e);
|
||||||
}
|
}
|
||||||
@@ -194,7 +196,9 @@ public class AutoGLMService extends AccessibilityService {
|
|||||||
focusNode.recycle();
|
focusNode.recycle();
|
||||||
return obj.toString();
|
return obj.toString();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {}
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "getFocusedNodeInfo failed", e);
|
||||||
|
}
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +265,9 @@ public class AutoGLMService extends AccessibilityService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {}
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "getCurrentApp failed", e);
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
private String stagingDir;
|
private String stagingDir;
|
||||||
private String homeDir;
|
private String homeDir;
|
||||||
private String binDir;
|
private String binDir;
|
||||||
private boolean isInstalling = false;
|
private volatile boolean isInstalling = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
@@ -203,39 +203,35 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
conn.connect();
|
conn.connect();
|
||||||
|
|
||||||
int total = conn.getContentLength();
|
int total = conn.getContentLength();
|
||||||
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
||||||
FileOutputStream out = new FileOutputStream(outputFile);
|
FileOutputStream out = new FileOutputStream(outputFile)) {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
long downloaded = 0;
|
||||||
|
int read;
|
||||||
|
long lastNotify = 0;
|
||||||
|
|
||||||
byte[] buffer = new byte[BUFFER_SIZE];
|
while ((read = in.read(buffer)) != -1) {
|
||||||
long downloaded = 0;
|
out.write(buffer, 0, read);
|
||||||
int read;
|
downloaded += read;
|
||||||
long lastNotify = 0;
|
long now = System.currentTimeMillis();
|
||||||
|
if (callback != null && now - lastNotify > 500) {
|
||||||
while ((read = in.read(buffer)) != -1) {
|
callback.onProgress(downloaded, total);
|
||||||
out.write(buffer, 0, read);
|
lastNotify = now;
|
||||||
downloaded += read;
|
}
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (callback != null && now - lastNotify > 500) {
|
|
||||||
callback.onProgress(downloaded, total);
|
|
||||||
lastNotify = now;
|
|
||||||
}
|
}
|
||||||
|
out.flush();
|
||||||
|
} finally {
|
||||||
|
conn.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
in.close();
|
|
||||||
conn.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
|
private List<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
|
||||||
List<String[]> symlinks = new ArrayList<>();
|
List<String[]> symlinks = new ArrayList<>();
|
||||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
|
ZipInputStream zis;
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
int extracted = 0;
|
int extracted = 0;
|
||||||
int total = 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);
|
java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile);
|
||||||
total = zf.size();
|
total = zf.size();
|
||||||
zf.close();
|
zf.close();
|
||||||
@@ -261,13 +257,13 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
} else {
|
} else {
|
||||||
File parent = outFile.getParentFile();
|
File parent = outFile.getParentFile();
|
||||||
if (parent != null) parent.mkdirs();
|
if (parent != null) parent.mkdirs();
|
||||||
FileOutputStream fos = new FileOutputStream(outFile);
|
try (FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||||
byte[] buf = new byte[BUFFER_SIZE];
|
byte[] buf = new byte[BUFFER_SIZE];
|
||||||
int len;
|
int len;
|
||||||
while ((len = zis.read(buf)) > 0) {
|
while ((len = zis.read(buf)) > 0) {
|
||||||
fos.write(buf, 0, len);
|
fos.write(buf, 0, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fos.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
extracted++;
|
extracted++;
|
||||||
@@ -597,78 +593,19 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String pythonBin = prefix + "/bin/python3";
|
JSObject setup = setupVirtualEnvInternal(prefix, home, venvDir);
|
||||||
String pythonBinAlt = prefix + "/bin/python";
|
String pipBin = setup.getString("pip");
|
||||||
|
if (pipBin == null || !new File(pipBin).exists()) {
|
||||||
if (!new File(pythonBin).exists() && !new File(pythonBinAlt).exists()) {
|
call.reject("Failed to prepare internal virtual environment");
|
||||||
Log.i(TAG, "Python not found, installing via pkg...");
|
|
||||||
String pkgBin = prefix + "/bin/pkg";
|
|
||||||
String aptBin = prefix + "/bin/apt";
|
|
||||||
String installBin = new File(pkgBin).exists() ? pkgBin : aptBin;
|
|
||||||
|
|
||||||
if (!new File(installBin).exists()) {
|
|
||||||
call.reject("Bootstrap not installed. Install dev tools first.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c",
|
|
||||||
"export PREFIX=\"" + prefix + "\" LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" HOME=\"" + home + "\" ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " +
|
|
||||||
"sh \"" + installBin + "\" install -y python clang rust make pkg-config libffi openssl 2>&1");
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
Process p = pb.start();
|
|
||||||
String installOutput = drainProcess(p);
|
|
||||||
p.waitFor(300, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
Log.i(TAG, "Python install output: " + installOutput.substring(0, Math.min(500, installOutput.length())));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!new File(pythonBin).exists() && !new File(pythonBinAlt).exists()) {
|
|
||||||
call.reject("Python installation failed. Try: pkg install python");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String usePython = new File(pythonBin).exists() ? pythonBin : pythonBinAlt;
|
String pipOutput = runBootstrapCommand(prefix, home,
|
||||||
|
"\"" + pipBin + "\" install --upgrade pip setuptools wheel && " +
|
||||||
|
"\"" + pipBin + "\" install --prefer-binary hermes-agent");
|
||||||
|
|
||||||
Log.i(TAG, "Creating Hermes Python venv...");
|
if (!new File(hermesLink).exists()) {
|
||||||
ProcessBuilder pb = new ProcessBuilder(usePython, "-m", "venv", venvDir);
|
call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(800, pipOutput.length())));
|
||||||
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
|
|
||||||
pb.environment().put("HOME", home);
|
|
||||||
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
|
|
||||||
pb.environment().put("PREFIX", prefix);
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
Process p = pb.start();
|
|
||||||
String venvOutput = drainProcess(p);
|
|
||||||
p.waitFor(60, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
Log.i(TAG, "venv output: " + venvOutput.substring(0, Math.min(300, venvOutput.length())));
|
|
||||||
|
|
||||||
String pipBin = venvDir + "/bin/pip";
|
|
||||||
if (!new File(pipBin).exists()) {
|
|
||||||
call.reject("Failed to create Python venv: " + venvOutput.substring(0, Math.min(200, venvOutput.length())));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Upgrading pip...");
|
|
||||||
pb = new ProcessBuilder(pipBin, "install", "--upgrade", "pip", "setuptools", "wheel");
|
|
||||||
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
|
|
||||||
pb.environment().put("HOME", home);
|
|
||||||
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
p = pb.start();
|
|
||||||
drainProcess(p);
|
|
||||||
p.waitFor(120, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
Log.i(TAG, "Installing hermes-agent...");
|
|
||||||
pb = new ProcessBuilder(pipBin, "install", "hermes-agent");
|
|
||||||
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
|
|
||||||
pb.environment().put("HOME", home);
|
|
||||||
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
|
|
||||||
pb.environment().put("PREFIX", prefix);
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
p = pb.start();
|
|
||||||
String pipOutput = drainProcess(p);
|
|
||||||
boolean pipOk = p.waitFor(600, java.util.concurrent.TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
if (!new File(venvDir + "/bin/hermes").exists()) {
|
|
||||||
call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(500, pipOutput.length())));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,6 +663,104 @@ public class BootstrapPlugin extends Plugin {
|
|||||||
}).start();
|
}).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 {
|
private String drainProcess(Process p) throws Exception {
|
||||||
java.io.InputStream is = p.getInputStream();
|
java.io.InputStream is = p.getInputStream();
|
||||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||||
|
|||||||
160
android/app/src/main/java/ai/z/chat/FileManagerPlugin.java
Normal file
160
android/app/src/main/java/ai/z/chat/FileManagerPlugin.java
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package ai.z.chat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
|
import com.getcapacitor.JSObject;
|
||||||
|
import com.getcapacitor.Plugin;
|
||||||
|
import com.getcapacitor.PluginCall;
|
||||||
|
import com.getcapacitor.PluginMethod;
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@CapacitorPlugin(name = "FileManager")
|
||||||
|
public class FileManagerPlugin extends Plugin {
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void openFile(PluginCall call) {
|
||||||
|
String path = call.getString("path", "");
|
||||||
|
String mimeType = call.getString("mimeType", "");
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
call.reject("No path provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists()) {
|
||||||
|
call.reject("File not found: " + path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Context context = getContext();
|
||||||
|
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
||||||
|
String resolvedMime = mimeType.isEmpty() ? guessMimeType(file.getName()) : mimeType;
|
||||||
|
if (resolvedMime == null || resolvedMime.isEmpty()) resolvedMime = "*/*";
|
||||||
|
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setDataAndType(uri, resolvedMime);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
context.startActivity(intent);
|
||||||
|
|
||||||
|
call.resolve(new JSObject().put("opened", true).put("path", file.getAbsolutePath()).put("mimeType", resolvedMime));
|
||||||
|
} catch (Exception e) {
|
||||||
|
call.reject("Open failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void listFiles(PluginCall call) {
|
||||||
|
String rootPath = call.getString("path", "");
|
||||||
|
if (rootPath.isEmpty()) {
|
||||||
|
call.reject("No path provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File root = new File(rootPath);
|
||||||
|
if (!root.exists()) {
|
||||||
|
call.reject("Path not found: " + rootPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSObject result = new JSObject();
|
||||||
|
result.put("path", root.getAbsolutePath());
|
||||||
|
result.put("name", root.getName());
|
||||||
|
result.put("directory", root.isDirectory());
|
||||||
|
result.put("size", root.isFile() ? root.length() : 0);
|
||||||
|
result.put("mimeType", root.isFile() ? guessMimeType(root.getName()) : "inode/directory");
|
||||||
|
|
||||||
|
if (root.isDirectory()) {
|
||||||
|
File[] children = root.listFiles();
|
||||||
|
List<JSObject> items = new ArrayList<>();
|
||||||
|
if (children != null) {
|
||||||
|
Arrays.sort(children, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
|
||||||
|
for (File child : children) {
|
||||||
|
JSObject item = new JSObject();
|
||||||
|
item.put("name", child.getName());
|
||||||
|
item.put("path", child.getAbsolutePath());
|
||||||
|
item.put("directory", child.isDirectory());
|
||||||
|
item.put("size", child.isFile() ? child.length() : 0);
|
||||||
|
item.put("mimeType", child.isFile() ? guessMimeType(child.getName()) : "inode/directory");
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.put("items", items);
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void openContainingFolder(PluginCall call) {
|
||||||
|
String path = call.getString("path", "");
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
call.reject("No path provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(path);
|
||||||
|
File target = file.isDirectory() ? file : file.getParentFile();
|
||||||
|
if (target == null || !target.exists()) {
|
||||||
|
call.reject("Containing folder not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve(new JSObject().put("opened", true).put("path", target.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void getRoots(PluginCall call) {
|
||||||
|
JSObject result = new JSObject();
|
||||||
|
List<JSObject> roots = new ArrayList<>();
|
||||||
|
|
||||||
|
roots.add(rootItem("App Files", getContext().getFilesDir()));
|
||||||
|
File ext = getContext().getExternalFilesDir(null);
|
||||||
|
if (ext != null) roots.add(rootItem("External Files", ext));
|
||||||
|
File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
if (downloads != null) roots.add(rootItem("Downloads", downloads));
|
||||||
|
File dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
||||||
|
if (dcim != null) roots.add(rootItem("Camera", dcim));
|
||||||
|
File sdcard = Environment.getExternalStorageDirectory();
|
||||||
|
if (sdcard != null) roots.add(rootItem("Storage Root", sdcard));
|
||||||
|
|
||||||
|
result.put("roots", roots);
|
||||||
|
call.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSObject rootItem(String label, File file) {
|
||||||
|
JSObject item = new JSObject();
|
||||||
|
item.put("name", label);
|
||||||
|
item.put("path", file.getAbsolutePath());
|
||||||
|
item.put("directory", true);
|
||||||
|
item.put("size", 0);
|
||||||
|
item.put("mimeType", "inode/directory");
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String guessMimeType(String name) {
|
||||||
|
String lower = name.toLowerCase();
|
||||||
|
if (lower.endsWith(".apk")) return "application/vnd.android.package-archive";
|
||||||
|
if (lower.endsWith(".apks")) return "application/vnd.android.package-archive";
|
||||||
|
if (lower.endsWith(".zip")) return "application/zip";
|
||||||
|
if (lower.endsWith(".json")) return "application/json";
|
||||||
|
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "text/html";
|
||||||
|
if (lower.endsWith(".md") || lower.endsWith(".txt") || lower.endsWith(".log")) return "text/plain";
|
||||||
|
String ext = MimeTypeMap.getFileExtensionFromUrl(name);
|
||||||
|
if (ext == null || ext.isEmpty()) return "*/*";
|
||||||
|
String guess = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
|
||||||
|
return guess != null ? guess : "*/*";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ import com.getcapacitor.PluginMethod;
|
|||||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
@CapacitorPlugin(name = "Installer")
|
@CapacitorPlugin(name = "Installer")
|
||||||
public class InstallerPlugin extends Plugin {
|
public class InstallerPlugin extends Plugin {
|
||||||
@@ -58,6 +61,45 @@ public class InstallerPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
public void exportApk(PluginCall call) {
|
||||||
|
String path = call.getString("path", "");
|
||||||
|
String name = call.getString("name", "app.apk");
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
call.reject("No path provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File apkFile = new File(path);
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
call.reject("APK file not found: " + path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Context context = getContext();
|
||||||
|
File outRoot = context.getExternalFilesDir(android.os.Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
if (outRoot == null) {
|
||||||
|
outRoot = context.getFilesDir();
|
||||||
|
}
|
||||||
|
File outDir = new File(outRoot, "ZAI-Chat");
|
||||||
|
if (!outDir.exists()) outDir.mkdirs();
|
||||||
|
File outFile = new File(outDir, name);
|
||||||
|
try (FileInputStream fis = new FileInputStream(apkFile);
|
||||||
|
FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int r;
|
||||||
|
while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r);
|
||||||
|
}
|
||||||
|
call.resolve(new JSObject()
|
||||||
|
.put("exported", true)
|
||||||
|
.put("path", outFile.getAbsolutePath())
|
||||||
|
.put("name", name));
|
||||||
|
} catch (Exception e) {
|
||||||
|
call.reject("Export failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
public void getDeviceInfo(PluginCall call) {
|
public void getDeviceInfo(PluginCall call) {
|
||||||
JSObject info = new JSObject();
|
JSObject info = new JSObject();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class MainActivity extends BridgeActivity {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
registerPlugin(ShellPlugin.class);
|
registerPlugin(ShellPlugin.class);
|
||||||
registerPlugin(InstallerPlugin.class);
|
registerPlugin(InstallerPlugin.class);
|
||||||
|
registerPlugin(FileManagerPlugin.class);
|
||||||
registerPlugin(WakePlugin.class);
|
registerPlugin(WakePlugin.class);
|
||||||
registerPlugin(BootstrapPlugin.class);
|
registerPlugin(BootstrapPlugin.class);
|
||||||
registerPlugin(AutoGLMPlugin.class);
|
registerPlugin(AutoGLMPlugin.class);
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ import com.getcapacitor.annotation.Permission;
|
|||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@CapacitorPlugin(name = "Shell",
|
@CapacitorPlugin(name = "Shell",
|
||||||
permissions = {
|
permissions = {
|
||||||
@@ -25,7 +24,7 @@ import java.util.Map;
|
|||||||
)
|
)
|
||||||
public class ShellPlugin extends Plugin {
|
public class ShellPlugin extends Plugin {
|
||||||
private static final String TAG = "ShellPlugin";
|
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 currentCwd = null;
|
||||||
private String homeDir = null;
|
private String homeDir = null;
|
||||||
private String toolsDir = null;
|
private String toolsDir = null;
|
||||||
@@ -228,9 +227,9 @@ public class ShellPlugin extends Plugin {
|
|||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
File parent = file.getParentFile();
|
File parent = file.getParentFile();
|
||||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
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.write(content);
|
||||||
writer.close();
|
}
|
||||||
call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length()));
|
call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
call.reject("Write failed: " + e.getMessage());
|
call.reject("Write failed: " + e.getMessage());
|
||||||
@@ -250,13 +249,13 @@ public class ShellPlugin extends Plugin {
|
|||||||
call.reject("File not found: " + path);
|
call.reject("File not found: " + path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
String line;
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"))) {
|
||||||
while ((line = reader.readLine()) != null) {
|
String line;
|
||||||
sb.append(line).append("\n");
|
while ((line = reader.readLine()) != null) {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reader.close();
|
|
||||||
call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath()));
|
call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
call.reject("Read failed: " + e.getMessage());
|
call.reject("Read failed: " + e.getMessage());
|
||||||
@@ -367,12 +366,15 @@ public class ShellPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION");
|
File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION");
|
||||||
if (versionFile.exists()) {
|
if (versionFile.exists()) {
|
||||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile));
|
String version;
|
||||||
String version = reader.readLine();
|
try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile))) {
|
||||||
reader.close();
|
version = reader.readLine();
|
||||||
|
}
|
||||||
return version != null ? version : "unknown";
|
return version != null ? version : "unknown";
|
||||||
}
|
}
|
||||||
} catch (Exception e) {}
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Unable to read Termux version", e);
|
||||||
|
}
|
||||||
return "installed";
|
return "installed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|||||||
public class WakePlugin extends Plugin {
|
public class WakePlugin extends Plugin {
|
||||||
private PowerManager.WakeLock screenWakeLock;
|
private PowerManager.WakeLock screenWakeLock;
|
||||||
private PowerManager.WakeLock cpuWakeLock;
|
private PowerManager.WakeLock cpuWakeLock;
|
||||||
private boolean isHeld = false;
|
private volatile boolean isHeld = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
@@ -29,11 +29,19 @@ public class WakePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
getActivity().runOnUiThread(() -> {
|
if (getActivity() != null) {
|
||||||
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getActivity().runOnUiThread(() -> {
|
||||||
});
|
if (getActivity() != null) {
|
||||||
|
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
|
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
|
||||||
|
if (pm == null) {
|
||||||
|
call.reject("Power service unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
screenWakeLock = pm.newWakeLock(
|
screenWakeLock = pm.newWakeLock(
|
||||||
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
|
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
|
||||||
@@ -57,9 +65,13 @@ public class WakePlugin extends Plugin {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
public void release(PluginCall call) {
|
public void release(PluginCall call) {
|
||||||
try {
|
try {
|
||||||
getActivity().runOnUiThread(() -> {
|
if (getActivity() != null) {
|
||||||
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getActivity().runOnUiThread(() -> {
|
||||||
});
|
if (getActivity() != null) {
|
||||||
|
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (screenWakeLock != null && screenWakeLock.isHeld()) {
|
if (screenWakeLock != null && screenWakeLock.isHeld()) {
|
||||||
screenWakeLock.release();
|
screenWakeLock.release();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "3.1.1",
|
"version": "3.3.0",
|
||||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Binary file not shown.
@@ -69,24 +69,24 @@
|
|||||||
<div id="chat-screen" class="screen">
|
<div id="chat-screen" class="screen">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<div class="header-left">
|
<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">
|
<div class="header-title">
|
||||||
<h2 id="conversation-title">New Chat</h2>
|
<h2 id="conversation-title">New Chat</h2>
|
||||||
<span id="current-mode-label" class="mode-label">Chat</span>
|
<span id="current-mode-label" class="mode-label">Chat</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="file-tree-btn" class="icon-btn" title="Project Files" style="display:none">📁</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">☾</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">+</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">⚙</button>
|
<button id="settings-btn" class="icon-btn" title="Settings" aria-label="Open settings">⚙</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sidebar" class="sidebar">
|
<div id="sidebar" class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>Conversations</h3>
|
<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>
|
||||||
<div id="conversation-list" class="conversation-list"></div>
|
<div id="conversation-list" class="conversation-list"></div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
@@ -98,7 +98,10 @@
|
|||||||
<div id="file-tree-panel" class="file-tree-panel">
|
<div id="file-tree-panel" class="file-tree-panel">
|
||||||
<div class="file-tree-header">
|
<div class="file-tree-header">
|
||||||
<h3>Project Files</h3>
|
<h3>Project Files</h3>
|
||||||
<button id="file-tree-close" class="icon-btn">×</button>
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<button id="device-files-btn" class="icon-btn" aria-label="Open device files" title="Device files">📁</button>
|
||||||
|
<button id="file-tree-close" class="icon-btn" aria-label="Close file tree">×</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-tree-body" class="file-tree-body">
|
<div id="file-tree-body" class="file-tree-body">
|
||||||
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
|
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
|
||||||
@@ -118,7 +121,7 @@
|
|||||||
<div class="file-viewer-actions">
|
<div class="file-viewer-actions">
|
||||||
<button id="file-viewer-edit" class="fv-btn">Edit</button>
|
<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-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>
|
</div>
|
||||||
<div id="file-viewer-body" class="file-viewer-body">
|
<div id="file-viewer-body" class="file-viewer-body">
|
||||||
@@ -153,10 +156,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<textarea id="message-input" placeholder="Type your message..." rows="1"></textarea>
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,11 +169,11 @@
|
|||||||
<div id="terminal-screen" class="screen">
|
<div id="terminal-screen" class="screen">
|
||||||
<div class="term-screen-container">
|
<div class="term-screen-container">
|
||||||
<div class="term-screen-header">
|
<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>
|
<h2>Terminal</h2>
|
||||||
<div class="term-screen-header-right">
|
<div class="term-screen-header-right">
|
||||||
<span id="term-cwd-display" class="term-cwd-display">~</span>
|
<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>
|
</div>
|
||||||
<div id="term-output" class="term-output"></div>
|
<div id="term-output" class="term-output"></div>
|
||||||
@@ -186,8 +189,8 @@
|
|||||||
<div class="term-input-row">
|
<div class="term-input-row">
|
||||||
<span class="term-prompt">$</span>
|
<span class="term-prompt">$</span>
|
||||||
<input type="text" id="term-input" class="term-input" placeholder="Enter command..." autocomplete="off" spellcheck="false">
|
<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-run-btn" class="term-run-btn" aria-label="Run terminal command">▶</button>
|
||||||
<button id="term-stop-btn" class="term-stop-btn" style="display:none">■</button>
|
<button id="term-stop-btn" class="term-stop-btn" aria-label="Stop terminal command" style="display:none">■</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,10 +234,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settings-screen" class="screen">
|
<div id="settings-screen" class="screen">
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<div class="settings-header">
|
<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>
|
<h2>Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-body">
|
<div class="settings-body">
|
||||||
@@ -338,13 +341,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
<p class="about-text">Z.AI Chat v3.1.0</p>
|
<p class="about-text">Z.AI Chat v3.3.0</p>
|
||||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||||
<p class="about-text">Compatible with Android 15/16</p>
|
<p class="about-text">Compatible with Android 15/16</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
<ul class="changelog-list">
|
<ul class="changelog-list">
|
||||||
|
<li>
|
||||||
|
<span class="changelog-version">v3.3.0</span>
|
||||||
|
<span class="changelog-date">2026-05-21</span>
|
||||||
|
<ul>
|
||||||
|
<li><strong>File Manager</strong> — browse device files, open/preview any file, install APKs directly</li>
|
||||||
|
<li><strong>SSH / Remote Access</strong> — AI can SSH into external machines, SCP files, curl URLs (user approves each)</li>
|
||||||
|
<li><strong>Approval Gate</strong> — all sensitive actions (SSH, SCP, curl, adb, pip, etc.) require your explicit approval</li>
|
||||||
|
<li><strong>New Action Tags</strong> — [SSH_EXEC], [SSH_UPLOAD], [SSH_DOWNLOAD], [REMOTE_EXEC], [CURL_EXEC]</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<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>
|
<li>
|
||||||
<span class="changelog-version">v3.1.0</span>
|
<span class="changelog-version">v3.1.0</span>
|
||||||
<span class="changelog-date">2026-05-20</span>
|
<span class="changelog-date">2026-05-20</span>
|
||||||
@@ -600,6 +625,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="approval-modal" class="file-viewer" style="display:none;z-index:3000;">
|
||||||
|
<div class="file-viewer-header">
|
||||||
|
<div class="file-viewer-title">
|
||||||
|
<span id="approval-title">Approval required</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-viewer-actions">
|
||||||
|
<button id="approval-deny" class="fv-btn">Deny</button>
|
||||||
|
<button id="approval-allow" class="fv-btn fv-btn-save">Approve</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-viewer-body" style="padding:16px;">
|
||||||
|
<pre id="approval-body" style="white-space:pre-wrap;margin:0;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="js/marked.min.js"></script>
|
<script src="js/marked.min.js"></script>
|
||||||
|
|||||||
448
www/js/app.js
448
www/js/app.js
@@ -6,9 +6,9 @@
|
|||||||
var STORAGE_KEY = 'zai_chat_';
|
var STORAGE_KEY = 'zai_chat_';
|
||||||
var MODE_PROMPTS = {
|
var MODE_PROMPTS = {
|
||||||
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
|
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.',
|
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 agent with FULL control of an Android device. You have a real terminal, can build APKs, control the device UI via AutoGLM, and use Hermes agent tools.\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## 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. Use [CREATE_FILE] for files, [BUILD_APK] to compile, [INSTALL_APK] to install\n2. Use [DEVICE_*] for device control — first [DEVICE_UI_TREE] to see screen, then interact\n3. Use [HERMES_EXEC] for Hermes capabilities — web search, terminal, skills, memory\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\n7. Never say done unless all files written, builds done, installs done'
|
agentic: 'You are an autonomous agent with full control of this app: internal Linux, terminal, virtual env, build tools, device UI, and EXTERNAL network access.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell (any command — runs inside app sandbox):\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## External SSH — run commands on remote machines:\n[SSH_EXEC user@host command]\n[SSH_UPLOAD local_path user@host:remote_path]\n[SSH_DOWNLOAD user@host:remote_path local_path]\n\n## External network tools:\n[REMOTE_EXEC host command]\n[CURL_EXEC url]\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 first\n2. Use SSH/REMOTE actions for external machines — user must approve each one\n3. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n4. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n5. Generate complete files, never stubs\n6. For Java: package ai.z.app, target SDK 36\n7. For SSH: assume openssh-client is available, use key-based auth when possible\n8. Output [TASK_COMPLETE] only when all work is done'
|
||||||
};
|
};
|
||||||
|
|
||||||
var BUILD_SCRIPT = [
|
var BUILD_SCRIPT = [
|
||||||
@@ -118,6 +118,8 @@
|
|||||||
terminalOpen: false,
|
terminalOpen: false,
|
||||||
keepAwake: false,
|
keepAwake: false,
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
autoInstallBuiltApk: true,
|
||||||
|
lastBuiltApkPath: '',
|
||||||
maxRetries: 10,
|
maxRetries: 10,
|
||||||
autoContinue: true,
|
autoContinue: true,
|
||||||
maxAutoContinue: 5
|
maxAutoContinue: 5
|
||||||
@@ -140,6 +142,7 @@
|
|||||||
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
|
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
|
||||||
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
|
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
|
||||||
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
|
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
|
||||||
|
state.autoInstallBuiltApk = localStorage.getItem(STORAGE_KEY + 'autoInstallBuiltApk') !== 'false';
|
||||||
state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10;
|
state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10;
|
||||||
state.autoContinue = localStorage.getItem(STORAGE_KEY + 'autoContinue') !== 'false';
|
state.autoContinue = localStorage.getItem(STORAGE_KEY + 'autoContinue') !== 'false';
|
||||||
state.maxAutoContinue = parseInt(localStorage.getItem(STORAGE_KEY + 'maxAutoContinue')) || 5;
|
state.maxAutoContinue = parseInt(localStorage.getItem(STORAGE_KEY + 'maxAutoContinue')) || 5;
|
||||||
@@ -163,6 +166,7 @@
|
|||||||
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
|
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
|
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString());
|
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString());
|
||||||
|
localStorage.setItem(STORAGE_KEY + 'autoInstallBuiltApk', state.autoInstallBuiltApk.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'maxRetries', state.maxRetries.toString());
|
localStorage.setItem(STORAGE_KEY + 'maxRetries', state.maxRetries.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'autoContinue', state.autoContinue.toString());
|
localStorage.setItem(STORAGE_KEY + 'autoContinue', state.autoContinue.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'maxAutoContinue', state.maxAutoContinue.toString());
|
localStorage.setItem(STORAGE_KEY + 'maxAutoContinue', state.maxAutoContinue.toString());
|
||||||
@@ -623,7 +627,7 @@
|
|||||||
if (state.streamingContent) {
|
if (state.streamingContent) {
|
||||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||||
}
|
}
|
||||||
var retryDiv = appendRetryMessage(err, requestBody, conv);
|
appendRetryMessage(err, requestBody, conv);
|
||||||
} else if (state.streamingContent) {
|
} else if (state.streamingContent) {
|
||||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||||
}
|
}
|
||||||
@@ -1180,16 +1184,19 @@
|
|||||||
autoglmEnabled: false,
|
autoglmEnabled: false,
|
||||||
commandQueue: []
|
commandQueue: []
|
||||||
};
|
};
|
||||||
|
var approvalState = null;
|
||||||
|
|
||||||
function initShellPlugins() {
|
function initShellPlugins() {
|
||||||
try {
|
try {
|
||||||
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
|
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
|
||||||
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
|
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
|
||||||
|
FileManager = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.FileManager;
|
||||||
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
|
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
|
||||||
Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
|
Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
if (!Shell) console.warn('Shell plugin not available');
|
if (!Shell) console.warn('Shell plugin not available');
|
||||||
if (!Installer) console.warn('Installer plugin not available');
|
if (!Installer) console.warn('Installer plugin not available');
|
||||||
|
if (!FileManager) console.warn('FileManager plugin not available');
|
||||||
if (!Wake) console.warn('Wake plugin not available');
|
if (!Wake) console.warn('Wake plugin not available');
|
||||||
if (!Bootstrap) console.warn('Bootstrap plugin not available');
|
if (!Bootstrap) console.warn('Bootstrap plugin not available');
|
||||||
}
|
}
|
||||||
@@ -1213,6 +1220,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function needsApprovalForCommand(command) {
|
||||||
|
var cmd = (command || '').trim();
|
||||||
|
return /(^|\s)(ssh|scp|sftp|curl|wget|adb|am\s+start|pm\s+install|install\s+|chmod|chown|rm\s+-rf|mv\s+|cp\s+|python|python3|pip|npm|git\s+push|git\s+pull)/i.test(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
async function shellWriteFile(path, content) {
|
async function shellWriteFile(path, content) {
|
||||||
if (!Shell) return false;
|
if (!Shell) return false;
|
||||||
try {
|
try {
|
||||||
@@ -1237,13 +1249,67 @@
|
|||||||
async function installApk(path) {
|
async function installApk(path) {
|
||||||
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
|
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
|
||||||
try {
|
try {
|
||||||
var result = await Installer.installApk({ path: path });
|
await Installer.installApk({ path: path });
|
||||||
termPrint('[APK install triggered: ' + path + ']', 'success');
|
termPrint('[APK install triggered: ' + path + ']', 'success');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
termPrint('[Install failed: ' + e.message + ']', 'err');
|
termPrint('[Install failed: ' + e.message + ']', 'err');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openFile(path, mimeType) {
|
||||||
|
if (!FileManager) { termPrint('[FileManager plugin not available]', 'err'); return; }
|
||||||
|
try {
|
||||||
|
await FileManager.openFile({ path: path, mimeType: mimeType || '' });
|
||||||
|
termPrint('[Open file triggered: ' + path + ']', 'success');
|
||||||
|
} catch(e) {
|
||||||
|
termPrint('[Open failed: ' + e.message + ']', 'err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestApproval(title, body, onApprove) {
|
||||||
|
approvalState = { onApprove: onApprove };
|
||||||
|
var modal = $('#approval-modal');
|
||||||
|
var titleEl = $('#approval-title');
|
||||||
|
var bodyEl = $('#approval-body');
|
||||||
|
if (titleEl) titleEl.textContent = title || 'Approval required';
|
||||||
|
if (bodyEl) bodyEl.textContent = body || '';
|
||||||
|
if (modal) modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeApproval() {
|
||||||
|
var modal = $('#approval-modal');
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
approvalState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askApproval(title, body, action) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
requestApproval(title, body, async function() {
|
||||||
|
try {
|
||||||
|
var result = await action();
|
||||||
|
resolve(result);
|
||||||
|
} catch(e) {
|
||||||
|
resolve(false);
|
||||||
|
} finally {
|
||||||
|
closeApproval();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listFiles(path) {
|
||||||
|
if (!FileManager) return null;
|
||||||
|
try { return await FileManager.listFiles({ path: path }); } catch(e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApkPath(path) {
|
||||||
|
return !!path && path.toLowerCase().endsWith('.apk');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpenablePath(path) {
|
||||||
|
return !!path && /\.(apk|apks|zip|json|txt|log|md|html?|xml|js|java|kt|png|jpg|jpeg|webp)$/i.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
async function getDeviceInfo() {
|
async function getDeviceInfo() {
|
||||||
if (!Installer) return {};
|
if (!Installer) return {};
|
||||||
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
|
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
|
||||||
@@ -1294,6 +1360,18 @@
|
|||||||
if (!command.trim()) return;
|
if (!command.trim()) return;
|
||||||
if (termState.isRunning) return;
|
if (termState.isRunning) return;
|
||||||
|
|
||||||
|
if (needsApprovalForCommand(command)) {
|
||||||
|
await askApproval('Run command?', command, function() {
|
||||||
|
return shellExec(command, termState.cwd, false).then(function(result) {
|
||||||
|
termPrint('$ ' + command, 'cmd');
|
||||||
|
if (result.output) termPrint(result.output.replace(/\n$/, ''), '');
|
||||||
|
if (result.exitCode !== 0 && result.exitCode !== undefined) termPrint('[exit code: ' + result.exitCode + ']', 'err');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
termState.history.push(command);
|
termState.history.push(command);
|
||||||
termState.historyIndex = termState.history.length;
|
termState.historyIndex = termState.history.length;
|
||||||
termPrint('$ ' + command, 'cmd');
|
termPrint('$ ' + command, 'cmd');
|
||||||
@@ -1439,8 +1517,16 @@
|
|||||||
var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi;
|
var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi;
|
||||||
var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi;
|
var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi;
|
||||||
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
|
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
|
||||||
|
var sshExecRegex = /\[SSH_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
|
||||||
|
var sshUploadRegex = /\[SSH_UPLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
|
||||||
|
var sshDownloadRegex = /\[SSH_DOWNLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
|
||||||
|
var remoteExecRegex = /\[REMOTE_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
|
||||||
|
var curlExecRegex = /\[CURL_EXEC\s+([^\]]+)\]/gi;
|
||||||
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
|
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
|
||||||
var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/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;
|
var match;
|
||||||
|
|
||||||
while ((match = createActionRegex.exec(content)) !== null) {
|
while ((match = createActionRegex.exec(content)) !== null) {
|
||||||
@@ -1500,6 +1586,27 @@
|
|||||||
while ((match = hermesExecRegex.exec(content)) !== null) {
|
while ((match = hermesExecRegex.exec(content)) !== null) {
|
||||||
actions.push({ type: 'hermes_exec', command: match[1].trim() });
|
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 = sshExecRegex.exec(content)) !== null) {
|
||||||
|
actions.push({ type: 'ssh_exec', host: match[1].trim(), command: match[2].trim() });
|
||||||
|
}
|
||||||
|
while ((match = sshUploadRegex.exec(content)) !== null) {
|
||||||
|
actions.push({ type: 'ssh_upload', localPath: match[1].trim(), remotePath: match[2].trim() });
|
||||||
|
}
|
||||||
|
while ((match = sshDownloadRegex.exec(content)) !== null) {
|
||||||
|
actions.push({ type: 'ssh_download', remotePath: match[1].trim(), localPath: match[2].trim() });
|
||||||
|
}
|
||||||
|
while ((match = remoteExecRegex.exec(content)) !== null) {
|
||||||
|
actions.push({ type: 'remote_exec', host: match[1].trim(), command: match[2].trim() });
|
||||||
|
}
|
||||||
|
while ((match = curlExecRegex.exec(content)) !== null) {
|
||||||
|
actions.push({ type: 'curl_exec', url: match[1].trim() });
|
||||||
|
}
|
||||||
while ((match = codeBlockFileRegex.exec(content)) !== null) {
|
while ((match = codeBlockFileRegex.exec(content)) !== null) {
|
||||||
var lang = match[1];
|
var lang = match[1];
|
||||||
var code = match[2];
|
var code = match[2];
|
||||||
@@ -1672,7 +1779,11 @@
|
|||||||
content.indexOf('[BUILD_APK') >= 0 ||
|
content.indexOf('[BUILD_APK') >= 0 ||
|
||||||
content.indexOf('[INSTALL_APK') >= 0 ||
|
content.indexOf('[INSTALL_APK') >= 0 ||
|
||||||
content.indexOf('[DEVICE_') >= 0 ||
|
content.indexOf('[DEVICE_') >= 0 ||
|
||||||
content.indexOf('[HERMES_') >= 0;
|
content.indexOf('[HERMES_') >= 0 ||
|
||||||
|
content.indexOf('[VENV_') >= 0 ||
|
||||||
|
content.indexOf('[SSH_') >= 0 ||
|
||||||
|
content.indexOf('[REMOTE_EXEC') >= 0 ||
|
||||||
|
content.indexOf('[CURL_EXEC') >= 0;
|
||||||
var hasCodeBlock = content.indexOf('```') >= 0;
|
var hasCodeBlock = content.indexOf('```') >= 0;
|
||||||
if (!hasCodeBlock && !hasAction && content.length < 300) return true;
|
if (!hasCodeBlock && !hasAction && content.length < 300) return true;
|
||||||
if ((content.match(/```/g) || []).length % 2 !== 0) return false;
|
if ((content.match(/```/g) || []).length % 2 !== 0) return false;
|
||||||
@@ -1832,6 +1943,9 @@
|
|||||||
html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">';
|
html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">';
|
||||||
html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>';
|
html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>';
|
||||||
html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>';
|
html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>';
|
||||||
|
if (isApkPath(file.path)) {
|
||||||
|
html += '<span class="ftree-badge">APK</span>';
|
||||||
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
@@ -1853,6 +1967,62 @@
|
|||||||
if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : '');
|
if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderDeviceFiles(rootPath) {
|
||||||
|
var body = $('#file-tree-body');
|
||||||
|
if (!body) return;
|
||||||
|
var result = await listFiles(rootPath);
|
||||||
|
if (!result) {
|
||||||
|
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = result.items || [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
body.innerHTML = '<div class="ftree-empty">Folder is empty.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<div class="device-files-root">';
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
|
||||||
|
html += '<span class="ftree-ext">' + escapeHtml(item.directory ? 'dir' : (item.mimeType || '?')) + '</span>';
|
||||||
|
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
|
||||||
|
html += item.directory ? '<span class="ftree-badge">DIR</span>' : (isApkPath(item.path) ? '<span class="ftree-badge">APK</span>' : '');
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
body.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderFileManagerHome() {
|
||||||
|
var body = $('#file-tree-body');
|
||||||
|
if (!body) return;
|
||||||
|
if (!FileManager) {
|
||||||
|
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var result = await FileManager.getRoots();
|
||||||
|
var roots = result.roots || [];
|
||||||
|
var html = '<div class="device-files-root">';
|
||||||
|
for (var i = 0; i < roots.length; i++) {
|
||||||
|
var item = roots[i];
|
||||||
|
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
|
||||||
|
html += '<span class="ftree-ext">root</span>';
|
||||||
|
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
|
||||||
|
html += '<span class="ftree-badge">OPEN</span>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
body.innerHTML = html;
|
||||||
|
var title = document.querySelector('#file-tree-panel h3');
|
||||||
|
if (title) title.textContent = 'File Manager';
|
||||||
|
var count = $('#file-tree-count');
|
||||||
|
if (count) count.textContent = roots.length + ' roots';
|
||||||
|
} catch(e) {
|
||||||
|
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleFileTree() {
|
function toggleFileTree() {
|
||||||
var panel = $('#file-tree-panel');
|
var panel = $('#file-tree-panel');
|
||||||
var overlay = $('#file-tree-overlay');
|
var overlay = $('#file-tree-overlay');
|
||||||
@@ -1868,6 +2038,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDeviceFileManager() {
|
||||||
|
var panel = $('#file-tree-panel');
|
||||||
|
var overlay = $('#file-tree-overlay');
|
||||||
|
if (!panel) return;
|
||||||
|
if (panel.classList.contains('open')) {
|
||||||
|
panel.classList.remove('open');
|
||||||
|
if (overlay) overlay.classList.remove('active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
panel.classList.add('open');
|
||||||
|
if (overlay) overlay.classList.add('active');
|
||||||
|
renderFileManagerHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProjectFiles() {
|
||||||
|
renderFileTree();
|
||||||
|
var title = document.querySelector('#file-tree-panel h3');
|
||||||
|
if (title) title.textContent = 'Project Files';
|
||||||
|
var count = $('#file-tree-count');
|
||||||
|
if (count) count.textContent = (getConversation() && getConversation().files ? getConversation().files.length : 0) + ' files';
|
||||||
|
}
|
||||||
|
|
||||||
function closeFileTree() {
|
function closeFileTree() {
|
||||||
var panel = $('#file-tree-panel');
|
var panel = $('#file-tree-panel');
|
||||||
var overlay = $('#file-tree-overlay');
|
var overlay = $('#file-tree-overlay');
|
||||||
@@ -1894,7 +2086,7 @@
|
|||||||
var saveBtn = $('#file-viewer-save');
|
var saveBtn = $('#file-viewer-save');
|
||||||
var editBtn = $('#file-viewer-edit');
|
var editBtn = $('#file-viewer-edit');
|
||||||
|
|
||||||
if (!viewer) return;
|
if (!viewer || !nameEl || !langEl || !contentEl || !textareaEl || !bodyEl || !editorEl || !saveBtn || !editBtn) return;
|
||||||
nameEl.textContent = file.path;
|
nameEl.textContent = file.path;
|
||||||
langEl.textContent = file.language;
|
langEl.textContent = file.language;
|
||||||
contentEl.textContent = file.content;
|
contentEl.textContent = file.content;
|
||||||
@@ -1910,6 +2102,19 @@
|
|||||||
viewer.dataset.path = path;
|
viewer.dataset.path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openFileFromTree(path) {
|
||||||
|
if (isApkPath(path)) {
|
||||||
|
await askApproval('Install APK?', path, function() { return installApk(path); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fileManagerTitle = document.querySelector('#file-tree-panel h3');
|
||||||
|
if (fileManagerTitle && fileManagerTitle.textContent === 'File Manager') {
|
||||||
|
await renderDeviceFiles(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await openFile(path, '');
|
||||||
|
}
|
||||||
|
|
||||||
function closeFileViewer() {
|
function closeFileViewer() {
|
||||||
var viewer = $('#file-viewer');
|
var viewer = $('#file-viewer');
|
||||||
if (viewer) viewer.style.display = 'none';
|
if (viewer) viewer.style.display = 'none';
|
||||||
@@ -2150,8 +2355,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tryProotExec(prootCmd, prefixUsr, pkgBin, aptBin) {
|
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';
|
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, "'\\''") + '\'';
|
var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\'';
|
||||||
|
|
||||||
termPrint('[*] Running pkg via PRoot...', 'info');
|
termPrint('[*] Running pkg via PRoot...', 'info');
|
||||||
@@ -2185,7 +2391,7 @@
|
|||||||
|
|
||||||
termPrint('[OK] Termux detected! Sending install command...', 'success');
|
termPrint('[OK] Termux detected! Sending install command...', 'success');
|
||||||
try {
|
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');
|
termPrint('[*] Command sent to Termux. Waiting...', 'info');
|
||||||
await new Promise(function(r) { setTimeout(r, 15000); });
|
await new Promise(function(r) { setTimeout(r, 15000); });
|
||||||
if (await toolsReady()) return true;
|
if (await toolsReady()) return true;
|
||||||
@@ -2313,9 +2519,11 @@
|
|||||||
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
|
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 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 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; });
|
||||||
|
var hasSsh = actions.some(function(a) { return a.type && a.type.indexOf('ssh_') === 0; });
|
||||||
|
var hasRemote = actions.some(function(a) { return a.type === 'remote_exec' || a.type === 'curl_exec'; });
|
||||||
|
|
||||||
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes) return;
|
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv && !hasSsh && !hasRemote) return;
|
||||||
|
|
||||||
_agenticRetryCount = 0;
|
_agenticRetryCount = 0;
|
||||||
var resultLog = [];
|
var resultLog = [];
|
||||||
|
|
||||||
@@ -2334,17 +2542,82 @@
|
|||||||
if (hasCommands) {
|
if (hasCommands) {
|
||||||
for (var c = 0; c < actions.length; c++) {
|
for (var c = 0; c < actions.length; c++) {
|
||||||
if (actions[c].type === 'run_command') {
|
if (actions[c].type === 'run_command') {
|
||||||
showStatusToast('Running: ' + actions[c].command.substring(0, 40), 'info');
|
var cmd = actions[c].command;
|
||||||
var cmdResult = await shellExec(actions[c].command, termState.cwd, false);
|
showStatusToast('Running: ' + cmd.substring(0, 40), 'info');
|
||||||
resultLog.push('CMD: ' + actions[c].command.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500));
|
var cmdApproved = true;
|
||||||
|
if (needsApprovalForCommand(cmd)) {
|
||||||
|
cmdApproved = await askApproval('Run command?', cmd, function() { return true; });
|
||||||
|
}
|
||||||
|
if (!cmdApproved) {
|
||||||
|
termPrint('[DENIED] ' + cmd, 'warning');
|
||||||
|
resultLog.push('DENIED: ' + cmd.substring(0, 60));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var cmdResult = await shellExec(cmd, termState.cwd, false);
|
||||||
|
resultLog.push('CMD: ' + cmd.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500));
|
||||||
if (cmdResult.output) {
|
if (cmdResult.output) {
|
||||||
termPrint('$ ' + actions[c].command, 'cmd');
|
termPrint('$ ' + cmd, 'cmd');
|
||||||
termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err');
|
termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasSsh) {
|
||||||
|
for (var s = 0; s < actions.length; s++) {
|
||||||
|
var sAct = actions[s];
|
||||||
|
if (sAct.type.indexOf('ssh_') !== 0) continue;
|
||||||
|
var sshCmd = '';
|
||||||
|
var sshLabel = '';
|
||||||
|
if (sAct.type === 'ssh_exec') {
|
||||||
|
sshCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.host + ' ' + sAct.command;
|
||||||
|
sshLabel = 'SSH: ' + sAct.host + ' → ' + sAct.command.substring(0, 60);
|
||||||
|
} else if (sAct.type === 'ssh_upload') {
|
||||||
|
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.localPath + ' ' + sAct.remotePath;
|
||||||
|
sshLabel = 'SCP upload: ' + sAct.localPath + ' → ' + sAct.remotePath;
|
||||||
|
} else if (sAct.type === 'ssh_download') {
|
||||||
|
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.remotePath + ' ' + sAct.localPath;
|
||||||
|
sshLabel = 'SCP download: ' + sAct.remotePath + ' → ' + sAct.localPath;
|
||||||
|
}
|
||||||
|
var sshOk = await askApproval(sshLabel, sshCmd, function() { return true; });
|
||||||
|
if (!sshOk) {
|
||||||
|
termPrint('[DENIED] ' + sshLabel, 'warning');
|
||||||
|
resultLog.push('DENIED: ' + sshLabel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
termPrint('$ ' + sshCmd, 'cmd');
|
||||||
|
var sshResult = await shellExec(sshCmd, termState.cwd, false);
|
||||||
|
resultLog.push('SSH: ' + sshLabel + '\nexit: ' + (sshResult.exitCode !== undefined ? sshResult.exitCode : '?') + '\n' + (sshResult.output || '').substring(0, 500));
|
||||||
|
termPrint((sshResult.output || '').replace(/\n$/, ''), sshResult.exitCode === 0 ? '' : 'err');
|
||||||
|
if (sshResult.exitCode !== 0 && sshResult.exitCode !== undefined) {
|
||||||
|
termPrint('[SSH exit: ' + sshResult.exitCode + ']', 'err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRemote) {
|
||||||
|
for (var r = 0; r < actions.length; r++) {
|
||||||
|
var rAct = actions[r];
|
||||||
|
if (rAct.type === 'remote_exec') {
|
||||||
|
var reCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + rAct.host + ' ' + rAct.command;
|
||||||
|
var reOk = await askApproval('Remote exec: ' + rAct.host, reCmd, function() { return true; });
|
||||||
|
if (!reOk) { termPrint('[DENIED] ' + rAct.host, 'warning'); continue; }
|
||||||
|
termPrint('$ ' + reCmd, 'cmd');
|
||||||
|
var reResult = await shellExec(reCmd, termState.cwd, false);
|
||||||
|
resultLog.push('REMOTE: ' + rAct.host + '\n' + (reResult.output || '').substring(0, 500));
|
||||||
|
termPrint((reResult.output || '').replace(/\n$/, ''), reResult.exitCode === 0 ? '' : 'err');
|
||||||
|
} else if (rAct.type === 'curl_exec') {
|
||||||
|
var curlCmd = 'curl -sL -o - ' + rAct.url;
|
||||||
|
var curlOk = await askApproval('Fetch URL?', rAct.url, function() { return true; });
|
||||||
|
if (!curlOk) { termPrint('[DENIED] curl ' + rAct.url, 'warning'); continue; }
|
||||||
|
termPrint('$ ' + curlCmd, 'cmd');
|
||||||
|
var curlResult = await shellExec(curlCmd, termState.cwd, false);
|
||||||
|
resultLog.push('CURL: ' + rAct.url + '\n' + (curlResult.output || '').substring(0, 500));
|
||||||
|
termPrint((curlResult.output || '').replace(/\n$/, ''), '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDevice) {
|
if (hasDevice) {
|
||||||
for (var d = 0; d < actions.length; d++) {
|
for (var d = 0; d < actions.length; d++) {
|
||||||
var act = actions[d];
|
var act = actions[d];
|
||||||
@@ -2375,6 +2648,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (hasBuild) {
|
||||||
showStatusToast('Building APK...', 'info');
|
showStatusToast('Building APK...', 'info');
|
||||||
var toolsReady = await ensureBuildTools();
|
var toolsReady = await ensureBuildTools();
|
||||||
@@ -2484,6 +2772,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async function autoDeployFile(action) {
|
||||||
var path = action.path;
|
var path = action.path;
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
@@ -2525,8 +2839,28 @@
|
|||||||
if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) {
|
if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) {
|
||||||
var sizeMatch = verifyResult.output.match(/(\d+)\s+/);
|
var sizeMatch = verifyResult.output.match(/(\d+)\s+/);
|
||||||
var sizeInfo = sizeMatch ? ' (' + Math.round(parseInt(sizeMatch[1]) / 1024) + ' KB)' : '';
|
var sizeInfo = sizeMatch ? ' (' + Math.round(parseInt(sizeMatch[1]) / 1024) + ' KB)' : '';
|
||||||
termPrint('\n[VERIFIED] APK built successfully: ' + apkPath + sizeInfo, 'success');
|
var builtName = projectDir.split('/').pop() + '.apk';
|
||||||
return '[BUILD OK] ' + apkPath;
|
var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
|
||||||
|
var exportedPath = apkPath;
|
||||||
|
try {
|
||||||
|
if (Installer) {
|
||||||
|
var exportResult = await Installer.exportApk({ path: apkPath, name: builtName });
|
||||||
|
exportedPath = exportResult.path || apkPath;
|
||||||
|
state.lastBuiltApkPath = exportedPath;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
termPrint('[!] Export to real folder failed: ' + e.message, 'warning');
|
||||||
|
}
|
||||||
|
termPrint('\n[VERIFIED] APK built successfully: ' + exportedPath + sizeInfo, 'success');
|
||||||
|
if (state.autoInstallBuiltApk && Installer) {
|
||||||
|
try {
|
||||||
|
await Installer.installApk({ path: exportedPath });
|
||||||
|
termPrint('[OK] Installer opened for built APK', 'success');
|
||||||
|
} catch(e) {
|
||||||
|
termPrint('[!] Auto-install failed: ' + e.message, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[BUILD OK] ' + exportedPath;
|
||||||
} else {
|
} else {
|
||||||
termPrint('\n[VERIFY FAILED] Build claimed success but APK not found at ' + apkPath, 'err');
|
termPrint('\n[VERIFY FAILED] Build claimed success but APK not found at ' + apkPath, 'err');
|
||||||
return '[BUILD FAILED] APK file not found after build. Output:\n' + output.substring(0, 1000);
|
return '[BUILD FAILED] APK file not found after build. Output:\n' + output.substring(0, 1000);
|
||||||
@@ -2625,55 +2959,14 @@
|
|||||||
showScreen('terminal');
|
showScreen('terminal');
|
||||||
termPrint('\n--- Building APK ---', 'info');
|
termPrint('\n--- Building APK ---', 'info');
|
||||||
|
|
||||||
var projectDir = termState.projectsDir || (termState.homeDir + '/projects');
|
var buildResult = await autoBuildApk(actions);
|
||||||
|
termPrint(buildResult, buildResult.indexOf('[BUILD FAILED]') >= 0 ? 'err' : 'success');
|
||||||
for (var i = 0; i < actions.length; i++) {
|
if (state.autoInstallBuiltApk && buildResult.indexOf('[BUILD OK]') >= 0) {
|
||||||
var action = actions[i];
|
var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
|
||||||
if (action.type === 'create_file') {
|
if (Installer && state.lastBuiltApkPath) {
|
||||||
var path = action.path;
|
await Installer.installApk({ path: state.lastBuiltApkPath });
|
||||||
if (!path.startsWith('/')) path = projectDir + '/' + path;
|
|
||||||
var dir = path.substring(0, path.lastIndexOf('/'));
|
|
||||||
await shellMkdirs(dir);
|
|
||||||
await shellWriteFile(path, action.content);
|
|
||||||
termPrint(' [+] ' + path, 'success');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
termPrint('\nBuilding with aapt2 + d8...', 'info');
|
|
||||||
|
|
||||||
var buildCmd = 'cd ' + projectDir + ' && ' +
|
|
||||||
'if [ -d "app/src/main" ]; then ' +
|
|
||||||
' AAPT2=$(which aapt2 2>/dev/null || echo "") && ' +
|
|
||||||
' D8=$(which d8 2>/dev/null || echo "") && ' +
|
|
||||||
' ECJ=$(which ecj 2>/dev/null || echo "") && ' +
|
|
||||||
' if [ -z "$AAPT2" ]; then echo "[!] aapt2 not found. Run Setup Dev Tools first."; exit 1; fi && ' +
|
|
||||||
' echo "[*] Compiling resources..." && ' +
|
|
||||||
' $AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
|
|
||||||
' echo "[*] Linking..." && ' +
|
|
||||||
' $AAPT2 link -o build/app.unsigned.apk ' +
|
|
||||||
' -I tools/android.jar ' +
|
|
||||||
' --manifest app/src/main/AndroidManifest.xml ' +
|
|
||||||
' -R build/compiled_resources.zip ' +
|
|
||||||
' --java build/gen 2>&1 && ' +
|
|
||||||
' echo "[*] Compiling Java..." && ' +
|
|
||||||
' find app/src/main/java -name "*.java" > build/sources.txt 2>/dev/null && ' +
|
|
||||||
' $ECJ -source 11 -target 11 -classpath tools/android.jar -d build/classes @build/sources.txt 2>&1 && ' +
|
|
||||||
' echo "[*] Converting to DEX..." && ' +
|
|
||||||
' $D8 --output build/ build/classes/**/*.class 2>&1 && ' +
|
|
||||||
' echo "[*] Packaging..." && ' +
|
|
||||||
' cd build && cp app.unsigned.apk app.unaligned.apk && ' +
|
|
||||||
' mkdir -p app.unaligned.apk.tmp && cd app.unaligned.apk.tmp && ' +
|
|
||||||
' unzip -o ../app.unaligned.apk && ' +
|
|
||||||
' cp ../classes.dex . && ' +
|
|
||||||
' zip -r ../app.unaligned.apk . && cd .. && rm -rf app.unaligned.apk.tmp && ' +
|
|
||||||
' echo "[*] Signing..." && ' +
|
|
||||||
' java -jar tools/uber-apk-signer.jar -a app.unaligned.apk --overwrite 2>&1 || ' +
|
|
||||||
' cp app.unaligned.apk app-signed.apk && ' +
|
|
||||||
' echo "[OK] APK built: ' + projectDir + '/build/app-signed.apk" && ' +
|
|
||||||
' echo "Size: $(du -h app-signed.apk | cut -f1)" ; ' +
|
|
||||||
'else echo "[!] No app/src/main found. Deploy files first."; fi';
|
|
||||||
|
|
||||||
await termExec(buildCmd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Dev Tools Setup ----
|
// ---- Dev Tools Setup ----
|
||||||
@@ -2715,7 +3008,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var result = await Bootstrap.install();
|
await Bootstrap.install();
|
||||||
|
|
||||||
progressText.textContent = 'Fixing file permissions...';
|
progressText.textContent = 'Fixing file permissions...';
|
||||||
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||||
@@ -2899,7 +3192,7 @@
|
|||||||
termState.projectsDir = env.PROJECTS;
|
termState.projectsDir = env.PROJECTS;
|
||||||
termState.cwd = env.CWD || env.HOME;
|
termState.cwd = env.CWD || env.HOME;
|
||||||
updateCwdDisplay();
|
updateCwdDisplay();
|
||||||
termPrint('Z.AI Terminal v1.3.0', 'info');
|
termPrint('Z.AI Terminal v3.3.0', 'info');
|
||||||
termPrint('Home: ' + termState.homeDir, 'info');
|
termPrint('Home: ' + termState.homeDir, 'info');
|
||||||
termPrint('Type "help" for commands, "setup" for dev tools\n', 'info');
|
termPrint('Type "help" for commands, "setup" for dev tools\n', 'info');
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
@@ -3185,13 +3478,30 @@
|
|||||||
saveState();
|
saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#file-tree-btn').addEventListener('click', toggleFileTree);
|
$('#file-tree-btn').addEventListener('click', toggleDeviceFileManager);
|
||||||
|
$('#device-files-btn').addEventListener('click', function() {
|
||||||
|
var panel = $('#file-tree-panel');
|
||||||
|
if (panel && panel.classList.contains('open')) {
|
||||||
|
showProjectFiles();
|
||||||
|
} else {
|
||||||
|
toggleDeviceFileManager();
|
||||||
|
}
|
||||||
|
});
|
||||||
$('#file-tree-close').addEventListener('click', closeFileTree);
|
$('#file-tree-close').addEventListener('click', closeFileTree);
|
||||||
$('#file-tree-overlay').addEventListener('click', closeFileTree);
|
$('#file-tree-overlay').addEventListener('click', closeFileTree);
|
||||||
$('#file-tree-body').addEventListener('click', function(e) {
|
$('#file-tree-body').addEventListener('click', function(e) {
|
||||||
var node = e.target.closest('.ftree-file');
|
var node = e.target.closest('.ftree-file');
|
||||||
if (node) {
|
if (node) {
|
||||||
openFileViewer(node.dataset.conv, node.dataset.path);
|
if (node.dataset.conv) {
|
||||||
|
openFileViewer(node.dataset.conv, node.dataset.path);
|
||||||
|
} else {
|
||||||
|
var title = document.querySelector('#file-tree-panel h3');
|
||||||
|
if (title && title.textContent === 'File Manager') {
|
||||||
|
renderDeviceFiles(node.dataset.path);
|
||||||
|
} else {
|
||||||
|
openFileFromTree(node.dataset.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
node = e.target.closest('.ftree-dir');
|
node = e.target.closest('.ftree-dir');
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -3205,6 +3515,10 @@
|
|||||||
$('#file-viewer-close').addEventListener('click', closeFileViewer);
|
$('#file-viewer-close').addEventListener('click', closeFileViewer);
|
||||||
$('#file-viewer-edit').addEventListener('click', toggleFileEdit);
|
$('#file-viewer-edit').addEventListener('click', toggleFileEdit);
|
||||||
$('#file-viewer-save').addEventListener('click', saveFileEdit);
|
$('#file-viewer-save').addEventListener('click', saveFileEdit);
|
||||||
|
$('#approval-allow').addEventListener('click', function() {
|
||||||
|
if (approvalState && approvalState.onApprove) approvalState.onApprove();
|
||||||
|
});
|
||||||
|
$('#approval-deny').addEventListener('click', closeApproval);
|
||||||
|
|
||||||
$('#theme-toggle-header').addEventListener('click', toggleTheme);
|
$('#theme-toggle-header').addEventListener('click', toggleTheme);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user