feat: 离线APK—Android Bridge本地执行(exec/listFiles/sysinfo)

This commit is contained in:
LukeMackin
2026-07-22 21:32:40 +08:00
parent a7dad7debc
commit c1a9e40d0a
3 changed files with 74 additions and 40 deletions

View File

@@ -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>

View File

@@ -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()
}
}
}

BIN
s.png Normal file

Binary file not shown.