feat: 离线APK—Android Bridge本地执行(exec/listFiles/sysinfo)
This commit is contained in:
@@ -11,48 +11,48 @@ h1{font-size:20px;color:#38bdf8;margin-bottom:4px}
|
||||
.btn{display:block;width:100%;padding:14px;margin:8px 0;border:none;border-radius:8px;font-size:15px;cursor:pointer;color:#fff}
|
||||
.btn-blue{background:#1e40af}
|
||||
.btn-green{background:#15803d}
|
||||
.btn-red{background:#991b1b}
|
||||
.input{display:block;width:100%;padding:12px;margin:8px 0;border:none;border-radius:8px;font-size:14px;background:#0f172a;color:#e2e8f0}
|
||||
.log{background:#0a0f1a;padding:12px;border-radius:8px;margin-top:8px;max-height:200px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#22c55e;font-family:monospace}
|
||||
.log{background:#0a0f1a;padding:12px;border-radius:8px;margin-top:8px;max-height:250px;overflow-y:auto;font-size:11px;white-space:pre-wrap;color:#22c55e;font-family:monospace}
|
||||
.tab{display:inline-block;padding:8px 16px;margin-right:4px;border-radius:6px 6px 0 0;background:#1e293b;color:#64748b;font-size:13px;cursor:pointer}
|
||||
.tab-on{background:#1e40af;color:#fff}
|
||||
</style></head><body>
|
||||
<h1>Yuzu GCA</h1>
|
||||
<div class="info">Self-contained MCP Client</div>
|
||||
<div class="info">Self-contained — no Gateway needed</div>
|
||||
|
||||
<div>
|
||||
<span class="tab tab-on" onclick="switchTab('exec')">Exec</span>
|
||||
<span class="tab" onclick="switchTab('files')">Files</span>
|
||||
<span class="tab" onclick="switchTab('sysinfo')">SysInfo</span>
|
||||
<span class="tab tab-on" onclick="switchTab('exec',event)">Exec</span>
|
||||
<span class="tab" onclick="switchTab('files',event)">Files</span>
|
||||
<span class="tab" onclick="switchTab('sysinfo',event)">SysInfo</span>
|
||||
</div>
|
||||
|
||||
<div class="card" id="tab-exec">
|
||||
<div class="card-title">Execute Command (Gateway)</div>
|
||||
<input class="input" id="gwUrl" placeholder="Gateway URL (http://x.x.x.x:3000)" value="http://10.1.0.17:3000">
|
||||
<input class="input" id="cmd" placeholder="Command (e.g. ls, whoami, dir)">
|
||||
<button class="btn btn-blue" onclick="execCmd()">Execute</button>
|
||||
<div class="card-title">Shell Command (local)</div>
|
||||
<input class="input" id="cmd" placeholder="e.g. ls, whoami, df -h">
|
||||
<button class="btn btn-blue" onclick="runExec()">Execute</button>
|
||||
<div class="log" id="log-exec"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="tab-files" style="display:none">
|
||||
<div class="card-title">List Files (Gateway)</div>
|
||||
<input class="input" id="path" placeholder="Path (e.g. /sdcard, .)" value=".">
|
||||
<button class="btn btn-blue" onclick="listFiles()">List</button>
|
||||
<div class="card-title">List Files (local)</div>
|
||||
<input class="input" id="path" placeholder="/sdcard or ." value=".">
|
||||
<button class="btn btn-blue" onclick="runFiles()">List</button>
|
||||
<div class="log" id="log-files"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="tab-sysinfo" style="display:none">
|
||||
<div class="card-title">System Info (Local)</div>
|
||||
<button class="btn btn-blue" onclick="showSysInfo()">Refresh</button>
|
||||
<div class="card-title">Device Info (local)</div>
|
||||
<button class="btn btn-blue" onclick="runSysinfo()">Refresh</button>
|
||||
<div class="log" id="log-sysinfo"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTab(t){
|
||||
document.querySelectorAll('.tab').forEach(e=>e.classList.toggle('tab-on',false));
|
||||
document.querySelectorAll('.card').forEach(e=>e.style.display='none');
|
||||
function switchTab(t,ev){
|
||||
ev.target.classList.add('tab-on');
|
||||
['exec','files','sysinfo'].filter(function(x){return x!==t}).forEach(function(x){
|
||||
document.getElementById('tab-'+x).style.display='none';
|
||||
document.querySelectorAll('.tab').forEach(function(b){if(b.textContent.toLowerCase().indexOf(x)>=0)b.classList.remove('tab-on')});
|
||||
});
|
||||
document.getElementById('tab-'+t).style.display='block';
|
||||
event.target.classList.toggle('tab-on',true);
|
||||
}
|
||||
|
||||
function log(id,msg){
|
||||
@@ -60,32 +60,25 @@ function log(id,msg){
|
||||
l.innerText+=msg+'\n';l.scrollTop=l.scrollHeight;
|
||||
}
|
||||
|
||||
function execCmd(){
|
||||
var gw=document.getElementById('gwUrl').value,c=document.getElementById('cmd').value;
|
||||
var id='log-exec';document.getElementById(id).innerText='';
|
||||
function runExec(){
|
||||
var c=document.getElementById('cmd').value, id='log-exec';
|
||||
document.getElementById(id).innerText='';
|
||||
log(id,'$ '+c);
|
||||
fetch(gw+'/tools/exec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({command:c})})
|
||||
.then(r=>r.json()).then(d=>log(id,d.stdout||d.stderr||JSON.stringify(d)))
|
||||
.catch(e=>log(id,'ERR: '+e.message));
|
||||
var r=Android.exec(c);
|
||||
log(id,r||'(empty)');
|
||||
}
|
||||
|
||||
function listFiles(){
|
||||
var gw=document.getElementById('gwUrl').value,p=document.getElementById('path').value;
|
||||
var id='log-files';document.getElementById(id).innerText='';
|
||||
function runFiles(){
|
||||
var p=document.getElementById('path').value, id='log-files';
|
||||
document.getElementById(id).innerText='';
|
||||
log(id,'ls '+p);
|
||||
fetch(gw+'/tools/file_list',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p})})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
if(d.entries){d.entries.forEach(e=>log(id,(e.type==='dir'?'[DIR]':'')+' '+e.name+(e.size?' ('+e.size+'B)':'')))}
|
||||
else log(id,JSON.stringify(d));
|
||||
}).catch(e=>log(id,'ERR: '+e.message));
|
||||
var r=Android.listFiles(p);
|
||||
log(id,r||'(empty)');
|
||||
}
|
||||
|
||||
function showSysInfo(){
|
||||
var id='log-sysinfo';document.getElementById(id).innerText='';
|
||||
log(id,'Platform: '+navigator.platform);
|
||||
log(id,'UserAgent: '+navigator.userAgent);
|
||||
log(id,'Online: '+navigator.onLine);
|
||||
if(typeof Android!=='undefined'){log(id,'Bridge: YES');log(id,'Files: '+Android.fetchText('version.txt'))}
|
||||
else log(id,'Bridge: NO');
|
||||
function runSysinfo(){
|
||||
var id='log-sysinfo';
|
||||
document.getElementById(id).innerText='';
|
||||
log(id,Android.sysinfo());
|
||||
}
|
||||
</script></body></html>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package dev.yuzu.gca
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import java.io.File
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -11,7 +15,44 @@ class MainActivity : AppCompatActivity() {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowUniversalAccessFromFileURLs = true
|
||||
addJavascriptInterface(LocalBridge(), "Android")
|
||||
loadUrl("file:///android_asset/index.html")
|
||||
}.also { setContentView(it) }
|
||||
}
|
||||
|
||||
inner class LocalBridge {
|
||||
@android.webkit.JavascriptInterface
|
||||
fun exec(cmd: String): String {
|
||||
return try {
|
||||
val p = Runtime.getRuntime().exec(arrayOf("sh", "-c", cmd))
|
||||
val out = BufferedReader(InputStreamReader(p.inputStream)).readText()
|
||||
val err = BufferedReader(InputStreamReader(p.errorStream)).readText()
|
||||
p.waitFor()
|
||||
if (out.isNotEmpty()) out else err
|
||||
} catch (e: Exception) { "ERR: ${e.message}" }
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
fun listFiles(path: String): String {
|
||||
return try {
|
||||
val dir = File(path)
|
||||
if (!dir.exists()) return "ERR: path not found"
|
||||
if (!dir.isDirectory) return "ERR: not a directory"
|
||||
dir.listFiles()?.joinToString("\n") { f ->
|
||||
(if (f.isDirectory) "[DIR] " else "[FILE] ") + f.name +
|
||||
(if (f.isFile) " (${f.length()}B)" else "")
|
||||
} ?: "empty"
|
||||
} catch (e: Exception) { "ERR: ${e.message}" }
|
||||
}
|
||||
|
||||
@android.webkit.JavascriptInterface
|
||||
fun sysinfo(): String {
|
||||
return """
|
||||
Device: ${android.os.Build.MODEL}
|
||||
Android: ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})
|
||||
CPU: ${Runtime.getRuntime().availableProcessors()} cores
|
||||
Memory: ${Runtime.getRuntime().maxMemory() / 1024 / 1024}MB max
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user