Compare commits
2 Commits
11
README.md
11
README.md
@@ -631,6 +631,17 @@ data: [DONE]
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.2.0 (2026-05-20)
|
||||
- **Full Internal Virtual Environment** — `BootstrapPlugin.setupVirtualEnv()` now provisions an app-contained Python venv under app storage, no external Termux app required
|
||||
- **In-App Module Installation** — `BootstrapPlugin.venvPipInstall()` installs Python modules directly into the internal venv
|
||||
- **New AI Automation Tags** — `[VENV_SETUP]` and `[VENV_PIP_INSTALL package]` available in coding and agentic modes
|
||||
- **Critical Runtime Fix** — replaced invalid JS `new File(...).exists` check with shell-based file existence test in PRoot strategy
|
||||
- **Concurrency Fix** — `ShellPlugin.activeProcesses` migrated to `ConcurrentHashMap` to avoid race/corruption under parallel process events
|
||||
- **Wake Stability** — added null-safe activity handling and power service guards in `WakePlugin`
|
||||
- **Bootstrap Resource Safety** — fixed major stream and descriptor leaks in download/extract paths (`downloadFile`, `extractBootstrap`)
|
||||
- **Accessibility Labels** — added missing `aria-label` attributes for icon-only controls in UI
|
||||
- **Version sync** — About screen and package/build versions updated to 3.2.0
|
||||
|
||||
### v3.1.0 (2026-05-20)
|
||||
- **AutoGLM Device Control** — `AccessibilityService` for full device automation: tap, swipe, long press, type text, screenshot, UI tree
|
||||
- **Navigation Controls** — press Back, Home, Recents, Notifications, Quick Settings, Power Dialog via global actions
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
def envOrDefault(String key, String fallback) {
|
||||
def fromEnv = System.getenv(key)
|
||||
if (fromEnv != null && fromEnv.trim()) return fromEnv.trim()
|
||||
if (project.hasProperty(key) && project.property(key)?.toString()?.trim()) return project.property(key).toString().trim()
|
||||
return fallback
|
||||
}
|
||||
|
||||
def debugStorePass = envOrDefault('ZAI_DEBUG_STORE_PASSWORD', 'android')
|
||||
def debugKeyAlias = envOrDefault('ZAI_DEBUG_KEY_ALIAS', 'androiddebugkey')
|
||||
def debugKeyPass = envOrDefault('ZAI_DEBUG_KEY_PASSWORD', 'android')
|
||||
def releaseStorePass = envOrDefault('ZAI_RELEASE_STORE_PASSWORD', 'zaichat')
|
||||
def releaseKeyAlias = envOrDefault('ZAI_RELEASE_KEY_ALIAS', 'zai-chat')
|
||||
def releaseKeyPass = envOrDefault('ZAI_RELEASE_KEY_PASSWORD', 'zaichat')
|
||||
|
||||
android {
|
||||
namespace = "ai.z.chat"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
@@ -7,8 +21,8 @@ android {
|
||||
applicationId "ai.z.chat"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 21
|
||||
versionName "3.1.1"
|
||||
versionCode 23
|
||||
versionName "3.2.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
@@ -18,15 +32,15 @@ android {
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
storePassword debugStorePass
|
||||
keyAlias debugKeyAlias
|
||||
keyPassword debugKeyPass
|
||||
}
|
||||
release {
|
||||
storeFile file('release.keystore')
|
||||
storePassword 'zaichat'
|
||||
keyAlias 'zai-chat'
|
||||
keyPassword 'zaichat'
|
||||
storePassword releaseStorePass
|
||||
keyAlias releaseKeyAlias
|
||||
keyPassword releaseKeyPass
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.getcapacitor.annotation.Permission;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
||||
@CapacitorPlugin(
|
||||
name = "AutoGLM",
|
||||
@@ -130,7 +129,11 @@ public class AutoGLMPlugin extends Plugin {
|
||||
destPath = getContext().getCacheDir() + "/autoglm_screenshot.png";
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.List;
|
||||
public class AutoGLMService extends AccessibilityService {
|
||||
|
||||
private static final String TAG = "AutoGLMService";
|
||||
private static AutoGLMService instance;
|
||||
private static volatile AutoGLMService instance;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
@@ -138,12 +138,14 @@ public class AutoGLMService extends AccessibilityService {
|
||||
screenshot.getHardwareBuffer(), screenshot.getColorSpace());
|
||||
if (bitmap != null) {
|
||||
Bitmap softwareBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
FileOutputStream fos = new FileOutputStream(destPath);
|
||||
softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
|
||||
fos.close();
|
||||
try (FileOutputStream fos = new FileOutputStream(destPath)) {
|
||||
softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
|
||||
}
|
||||
softwareBitmap.recycle();
|
||||
}
|
||||
screenshot.getHardwareBuffer().close();
|
||||
if (screenshot.getHardwareBuffer() != null) {
|
||||
screenshot.getHardwareBuffer().close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Screenshot save failed", e);
|
||||
}
|
||||
@@ -194,7 +196,9 @@ public class AutoGLMService extends AccessibilityService {
|
||||
focusNode.recycle();
|
||||
return obj.toString();
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "getFocusedNodeInfo failed", e);
|
||||
}
|
||||
return "{}";
|
||||
}
|
||||
|
||||
@@ -261,7 +265,9 @@ public class AutoGLMService extends AccessibilityService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "getCurrentApp failed", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public class BootstrapPlugin extends Plugin {
|
||||
private String stagingDir;
|
||||
private String homeDir;
|
||||
private String binDir;
|
||||
private boolean isInstalling = false;
|
||||
private volatile boolean isInstalling = false;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
@@ -203,39 +203,35 @@ public class BootstrapPlugin extends Plugin {
|
||||
conn.connect();
|
||||
|
||||
int total = conn.getContentLength();
|
||||
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
||||
FileOutputStream out = new FileOutputStream(outputFile);
|
||||
try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
|
||||
FileOutputStream out = new FileOutputStream(outputFile)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
long downloaded = 0;
|
||||
int read;
|
||||
long lastNotify = 0;
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
long downloaded = 0;
|
||||
int read;
|
||||
long lastNotify = 0;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
downloaded += read;
|
||||
long now = System.currentTimeMillis();
|
||||
if (callback != null && now - lastNotify > 500) {
|
||||
callback.onProgress(downloaded, total);
|
||||
lastNotify = now;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
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 {
|
||||
List<String[]> symlinks = new ArrayList<>();
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
|
||||
ZipInputStream zis;
|
||||
ZipEntry entry;
|
||||
int extracted = 0;
|
||||
int total = 0;
|
||||
|
||||
java.util.Enumeration<java.util.zip.ZipEntry> entries = java.util.Collections.emptyEnumeration();
|
||||
|
||||
java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile);
|
||||
total = zf.size();
|
||||
zf.close();
|
||||
@@ -261,13 +257,13 @@ public class BootstrapPlugin extends Plugin {
|
||||
} else {
|
||||
File parent = outFile.getParentFile();
|
||||
if (parent != null) parent.mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(outFile);
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
while ((len = zis.read(buf)) > 0) {
|
||||
fos.write(buf, 0, len);
|
||||
try (FileOutputStream fos = new FileOutputStream(outFile)) {
|
||||
byte[] buf = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
while ((len = zis.read(buf)) > 0) {
|
||||
fos.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
fos.close();
|
||||
}
|
||||
}
|
||||
extracted++;
|
||||
@@ -726,6 +722,93 @@ public class BootstrapPlugin extends Plugin {
|
||||
}).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");
|
||||
|
||||
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()) {
|
||||
call.reject("Bootstrap tools missing. Install internal dev environment first.");
|
||||
return;
|
||||
}
|
||||
|
||||
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()) {
|
||||
call.reject("python3 unavailable after install");
|
||||
return;
|
||||
}
|
||||
|
||||
runBootstrapCommand(prefix, home,
|
||||
"\"" + python3 + "\" -m venv \"" + venvDir + "\" && " +
|
||||
"\"" + venvDir + "/bin/pip\" install --upgrade pip setuptools wheel");
|
||||
|
||||
call.resolve(new JSObject()
|
||||
.put("ok", true)
|
||||
.put("venv", venvDir)
|
||||
.put("python", venvDir + "/bin/python")
|
||||
.put("pip", venvDir + "/bin/pip"));
|
||||
} 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 " + 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 String runBootstrapCommand(String prefix, String home, String command) throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c",
|
||||
"export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " +
|
||||
"LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" " +
|
||||
"ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " + command);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String out = drainProcess(p);
|
||||
int code = p.waitFor();
|
||||
if (code != 0) throw new RuntimeException("cmd failed(" + code + "): " + out);
|
||||
return out;
|
||||
}
|
||||
|
||||
private String drainProcess(Process p) throws Exception {
|
||||
java.io.InputStream is = p.getInputStream();
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
|
||||
@@ -13,10 +13,9 @@ import com.getcapacitor.annotation.Permission;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@CapacitorPlugin(name = "Shell",
|
||||
permissions = {
|
||||
@@ -25,7 +24,7 @@ import java.util.Map;
|
||||
)
|
||||
public class ShellPlugin extends Plugin {
|
||||
private static final String TAG = "ShellPlugin";
|
||||
private final Map<String, Process> activeProcesses = new HashMap<>();
|
||||
private final Map<String, Process> activeProcesses = new ConcurrentHashMap<>();
|
||||
private String currentCwd = null;
|
||||
private String homeDir = null;
|
||||
private String toolsDir = null;
|
||||
@@ -228,9 +227,9 @@ public class ShellPlugin extends Plugin {
|
||||
File file = new File(path);
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
java.io.FileWriter writer = new java.io.FileWriter(file);
|
||||
writer.write(content);
|
||||
writer.close();
|
||||
try (java.io.FileWriter writer = new java.io.FileWriter(file)) {
|
||||
writer.write(content);
|
||||
}
|
||||
call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length()));
|
||||
} catch (Exception e) {
|
||||
call.reject("Write failed: " + e.getMessage());
|
||||
@@ -250,13 +249,13 @@ public class ShellPlugin extends Plugin {
|
||||
call.reject("File not found: " + path);
|
||||
return;
|
||||
}
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
reader.close();
|
||||
call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath()));
|
||||
} catch (Exception e) {
|
||||
call.reject("Read failed: " + e.getMessage());
|
||||
@@ -367,12 +366,15 @@ public class ShellPlugin extends Plugin {
|
||||
try {
|
||||
File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION");
|
||||
if (versionFile.exists()) {
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile));
|
||||
String version = reader.readLine();
|
||||
reader.close();
|
||||
String version;
|
||||
try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile))) {
|
||||
version = reader.readLine();
|
||||
}
|
||||
return version != null ? version : "unknown";
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Unable to read Termux version", e);
|
||||
}
|
||||
return "installed";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
public class WakePlugin extends Plugin {
|
||||
private PowerManager.WakeLock screenWakeLock;
|
||||
private PowerManager.WakeLock cpuWakeLock;
|
||||
private boolean isHeld = false;
|
||||
private volatile boolean isHeld = false;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
@@ -29,11 +29,19 @@ public class WakePlugin extends Plugin {
|
||||
}
|
||||
|
||||
try {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
});
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
|
||||
if (pm == null) {
|
||||
call.reject("Power service unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
screenWakeLock = pm.newWakeLock(
|
||||
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
|
||||
@@ -57,9 +65,13 @@ public class WakePlugin extends Plugin {
|
||||
@PluginMethod
|
||||
public void release(PluginCall call) {
|
||||
try {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
});
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (screenWakeLock != null && screenWakeLock.isHeld()) {
|
||||
screenWakeLock.release();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zai-chat",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
BIN
releases/Z.AI-Chat-v3.1.2-release.apk.idsig
Normal file
BIN
releases/Z.AI-Chat-v3.1.2-release.apk.idsig
Normal file
Binary file not shown.
@@ -69,24 +69,24 @@
|
||||
<div id="chat-screen" class="screen">
|
||||
<div class="chat-header">
|
||||
<div class="header-left">
|
||||
<button id="menu-btn" class="icon-btn">☰</button>
|
||||
<button id="menu-btn" class="icon-btn" aria-label="Open sidebar menu">☰</button>
|
||||
<div class="header-title">
|
||||
<h2 id="conversation-title">New Chat</h2>
|
||||
<span id="current-mode-label" class="mode-label">Chat</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="file-tree-btn" class="icon-btn" title="Project Files" style="display:none">📁</button>
|
||||
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">☾</button>
|
||||
<button id="new-chat-btn" class="icon-btn" title="New chat">+</button>
|
||||
<button id="settings-btn" class="icon-btn" title="Settings">⚙</button>
|
||||
<button id="file-tree-btn" class="icon-btn" title="Project Files" aria-label="Open project files" style="display:none">📁</button>
|
||||
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme" aria-label="Toggle theme">☾</button>
|
||||
<button id="new-chat-btn" class="icon-btn" title="New chat" aria-label="Start new chat">+</button>
|
||||
<button id="settings-btn" class="icon-btn" title="Settings" aria-label="Open settings">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>Conversations</h3>
|
||||
<button id="sidebar-close" class="icon-btn">×</button>
|
||||
<button id="sidebar-close" class="icon-btn" aria-label="Close sidebar">×</button>
|
||||
</div>
|
||||
<div id="conversation-list" class="conversation-list"></div>
|
||||
<div class="sidebar-footer">
|
||||
@@ -98,7 +98,7 @@
|
||||
<div id="file-tree-panel" class="file-tree-panel">
|
||||
<div class="file-tree-header">
|
||||
<h3>Project Files</h3>
|
||||
<button id="file-tree-close" class="icon-btn">×</button>
|
||||
<button id="file-tree-close" class="icon-btn" aria-label="Close file tree">×</button>
|
||||
</div>
|
||||
<div id="file-tree-body" class="file-tree-body">
|
||||
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="file-viewer-actions">
|
||||
<button id="file-viewer-edit" class="fv-btn">Edit</button>
|
||||
<button id="file-viewer-save" class="fv-btn fv-btn-save" style="display:none">Save</button>
|
||||
<button id="file-viewer-close" class="icon-btn">×</button>
|
||||
<button id="file-viewer-close" class="icon-btn" aria-label="Close file viewer">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="file-viewer-body" class="file-viewer-body">
|
||||
@@ -153,10 +153,10 @@
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<textarea id="message-input" placeholder="Type your message..." rows="1"></textarea>
|
||||
<button id="send-btn" class="send-btn" disabled>
|
||||
<button id="send-btn" class="send-btn" aria-label="Send message" disabled>
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button id="stop-btn" class="stop-btn" style="display:none">
|
||||
<button id="stop-btn" class="stop-btn" aria-label="Stop generation" style="display:none">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="6" y="6" width="12" height="12" fill="currentColor" rx="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -166,11 +166,11 @@
|
||||
<div id="terminal-screen" class="screen">
|
||||
<div class="term-screen-container">
|
||||
<div class="term-screen-header">
|
||||
<button id="term-back-btn" class="icon-btn">←</button>
|
||||
<button id="term-back-btn" class="icon-btn" aria-label="Back to chat">←</button>
|
||||
<h2>Terminal</h2>
|
||||
<div class="term-screen-header-right">
|
||||
<span id="term-cwd-display" class="term-cwd-display">~</span>
|
||||
<button id="term-setup-tools-btn" class="icon-btn" title="Setup Dev Tools">🛠</button>
|
||||
<button id="term-setup-tools-btn" class="icon-btn" title="Setup Dev Tools" aria-label="Setup development tools">🛠</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="term-output" class="term-output"></div>
|
||||
@@ -186,8 +186,8 @@
|
||||
<div class="term-input-row">
|
||||
<span class="term-prompt">$</span>
|
||||
<input type="text" id="term-input" class="term-input" placeholder="Enter command..." autocomplete="off" spellcheck="false">
|
||||
<button id="term-run-btn" class="term-run-btn">▶</button>
|
||||
<button id="term-stop-btn" class="term-stop-btn" style="display:none">■</button>
|
||||
<button id="term-run-btn" class="term-run-btn" aria-label="Run terminal command">▶</button>
|
||||
<button id="term-stop-btn" class="term-stop-btn" aria-label="Stop terminal command" style="display:none">■</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +234,7 @@
|
||||
<div id="settings-screen" class="screen">
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<button id="settings-back" class="icon-btn">←</button>
|
||||
<button id="settings-back" class="icon-btn" aria-label="Back to chat">←</button>
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
@@ -338,13 +338,25 @@
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>About</h3>
|
||||
<p class="about-text">Z.AI Chat v3.1.0</p>
|
||||
<p class="about-text">Z.AI Chat v3.2.0</p>
|
||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||
<p class="about-text">Compatible with Android 15/16</p>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Changelog</h3>
|
||||
<ul class="changelog-list">
|
||||
<li>
|
||||
<span class="changelog-version">v3.2.0</span>
|
||||
<span class="changelog-date">2026-05-20</span>
|
||||
<ul>
|
||||
<li><strong>Full In-App Virtual Env</strong> — new internal virtual environment setup with no external Termux app dependency</li>
|
||||
<li><strong>Module Installer</strong> — new AI actions: <code>[VENV_SETUP]</code> and <code>[VENV_PIP_INSTALL package]</code></li>
|
||||
<li><strong>QA Critical Fix</strong> — fixed invalid JS file check that broke PRoot install fallback</li>
|
||||
<li><strong>Stability</strong> — thread-safe process map in ShellPlugin and safer wake lock handling</li>
|
||||
<li><strong>Resource Safety</strong> — fixed high-risk stream/file descriptor leaks in bootstrap extraction and downloads</li>
|
||||
<li><strong>Accessibility</strong> — improved icon button labels for better screen reader support</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<span class="changelog-version">v3.1.0</span>
|
||||
<span class="changelog-date">2026-05-20</span>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
var STORAGE_KEY = 'zai_chat_';
|
||||
var MODE_PROMPTS = {
|
||||
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
|
||||
coding: 'You are an expert coding assistant. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.',
|
||||
coding: 'You are an expert coding assistant with internal tool access. Use action tags when execution is needed: [CREATE_FILE ...][/CREATE_FILE], [RUN_COMMAND]...[/RUN_COMMAND], [BUILD_APK project], [INSTALL_APK path], [VENV_SETUP], [VENV_PIP_INSTALL package]. Keep responses concise and complete.',
|
||||
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
|
||||
agentic: 'You are an autonomous 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 sandbox: internal Linux bootstrap, terminal, virtual env, build tools, and device UI via AutoGLM.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell:\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\n\n## In-App Virtual Env:\n[VENV_SETUP]\n[VENV_PIP_INSTALL package_name]\n\n## AutoGLM Device Control:\n[DEVICE_TAP x y]\n[DEVICE_LONG_PRESS x y]\n[DEVICE_SWIPE startX startY endX endY]\n[DEVICE_TYPE text]\n[DEVICE_PRESS_BACK]\n[DEVICE_PRESS_HOME]\n[DEVICE_PRESS_RECENTS]\n[DEVICE_SCREENSHOT]\n[DEVICE_UI_TREE]\n[DEVICE_CLICK_TEXT button text]\n[DEVICE_CLICK_ID com.example:id/viewId]\n[DEVICE_LAUNCH com.example.app]\n[DEVICE_CURRENT_APP]\n\n## Hermes Agent:\n[HERMES_INSTALL]\n[HERMES_EXEC command]\n\n## Rules:\n1. Prefer internal sandbox tools and virtual env first\n2. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n3. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n4. Generate complete files, never stubs\n5. For Java: package ai.z.app, target SDK 36\n6. Output [TASK_COMPLETE] only when all work is done'
|
||||
};
|
||||
|
||||
var BUILD_SCRIPT = [
|
||||
@@ -623,7 +623,7 @@
|
||||
if (state.streamingContent) {
|
||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||
}
|
||||
var retryDiv = appendRetryMessage(err, requestBody, conv);
|
||||
appendRetryMessage(err, requestBody, conv);
|
||||
} else if (state.streamingContent) {
|
||||
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
|
||||
}
|
||||
@@ -1237,7 +1237,7 @@
|
||||
async function installApk(path) {
|
||||
if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; }
|
||||
try {
|
||||
var result = await Installer.installApk({ path: path });
|
||||
await Installer.installApk({ path: path });
|
||||
termPrint('[APK install triggered: ' + path + ']', 'success');
|
||||
} catch(e) {
|
||||
termPrint('[Install failed: ' + e.message + ']', 'err');
|
||||
@@ -1441,6 +1441,9 @@
|
||||
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
|
||||
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
|
||||
var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/gi;
|
||||
var venvSetupRegex = /\[VENV_SETUP\]/gi;
|
||||
var venvPipInstallRegex = /\[VENV_PIP_INSTALL\s+([^\]]+)\]/gi;
|
||||
var codeBlockFileRegex = /```(\w+)\s*\n([\s\S]*?)```/gi;
|
||||
var match;
|
||||
|
||||
while ((match = createActionRegex.exec(content)) !== null) {
|
||||
@@ -1500,6 +1503,12 @@
|
||||
while ((match = hermesExecRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'hermes_exec', command: match[1].trim() });
|
||||
}
|
||||
while ((match = venvSetupRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'venv_setup' });
|
||||
}
|
||||
while ((match = venvPipInstallRegex.exec(content)) !== null) {
|
||||
actions.push({ type: 'venv_pip_install', packages: match[1].trim() });
|
||||
}
|
||||
while ((match = codeBlockFileRegex.exec(content)) !== null) {
|
||||
var lang = match[1];
|
||||
var code = match[2];
|
||||
@@ -1672,7 +1681,8 @@
|
||||
content.indexOf('[BUILD_APK') >= 0 ||
|
||||
content.indexOf('[INSTALL_APK') >= 0 ||
|
||||
content.indexOf('[DEVICE_') >= 0 ||
|
||||
content.indexOf('[HERMES_') >= 0;
|
||||
content.indexOf('[HERMES_') >= 0 ||
|
||||
content.indexOf('[VENV_') >= 0;
|
||||
var hasCodeBlock = content.indexOf('```') >= 0;
|
||||
if (!hasCodeBlock && !hasAction && content.length < 300) return true;
|
||||
if ((content.match(/```/g) || []).length % 2 !== 0) return false;
|
||||
@@ -1894,7 +1904,7 @@
|
||||
var saveBtn = $('#file-viewer-save');
|
||||
var editBtn = $('#file-viewer-edit');
|
||||
|
||||
if (!viewer) return;
|
||||
if (!viewer || !nameEl || !langEl || !contentEl || !textareaEl || !bodyEl || !editorEl || !saveBtn || !editBtn) return;
|
||||
nameEl.textContent = file.path;
|
||||
langEl.textContent = file.language;
|
||||
contentEl.textContent = file.content;
|
||||
@@ -2150,8 +2160,9 @@
|
||||
}
|
||||
|
||||
async function tryProotExec(prootCmd, prefixUsr, pkgBin, aptBin) {
|
||||
var hasPkg = await shellExec('test -f "' + pkgBin + '"', termState.homeDir, false);
|
||||
var pkgCmd = 'sh /usr/bin/pkg update -y 2>&1 && sh /usr/bin/pkg install -y aapt2 ecj dx apksigner 2>&1';
|
||||
if (!new File(pkgBin).exists) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1';
|
||||
if (hasPkg.exitCode !== 0) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1';
|
||||
var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\'';
|
||||
|
||||
termPrint('[*] Running pkg via PRoot...', 'info');
|
||||
@@ -2185,7 +2196,7 @@
|
||||
|
||||
termPrint('[OK] Termux detected! Sending install command...', 'success');
|
||||
try {
|
||||
var runResult = await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'});
|
||||
await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'});
|
||||
termPrint('[*] Command sent to Termux. Waiting...', 'info');
|
||||
await new Promise(function(r) { setTimeout(r, 15000); });
|
||||
if (await toolsReady()) return true;
|
||||
@@ -2313,8 +2324,9 @@
|
||||
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
|
||||
var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
|
||||
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
|
||||
var hasVenv = actions.some(function(a) { return a.type && a.type.indexOf('venv_') === 0; });
|
||||
|
||||
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes) return;
|
||||
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv) return;
|
||||
|
||||
_agenticRetryCount = 0;
|
||||
var resultLog = [];
|
||||
@@ -2375,6 +2387,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) {
|
||||
showStatusToast('Building APK...', 'info');
|
||||
var toolsReady = await ensureBuildTools();
|
||||
@@ -2484,6 +2511,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) {
|
||||
var path = action.path;
|
||||
if (!path.startsWith('/')) {
|
||||
@@ -2715,7 +2768,7 @@
|
||||
});
|
||||
|
||||
try {
|
||||
var result = await Bootstrap.install();
|
||||
await Bootstrap.install();
|
||||
|
||||
progressText.textContent = 'Fixing file permissions...';
|
||||
try { await Bootstrap.fixPermissions(); } catch(e) {}
|
||||
|
||||
Reference in New Issue
Block a user