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{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-blue{background:#1e40af}
|
||||||
.btn-green{background:#15803d}
|
.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}
|
.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{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}
|
.tab-on{background:#1e40af;color:#fff}
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<h1>Yuzu GCA</h1>
|
<h1>Yuzu GCA</h1>
|
||||||
<div class="info">Self-contained MCP Client</div>
|
<div class="info">Self-contained — no Gateway needed</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="tab tab-on" onclick="switchTab('exec')">Exec</span>
|
<span class="tab tab-on" onclick="switchTab('exec',event)">Exec</span>
|
||||||
<span class="tab" onclick="switchTab('files')">Files</span>
|
<span class="tab" onclick="switchTab('files',event)">Files</span>
|
||||||
<span class="tab" onclick="switchTab('sysinfo')">SysInfo</span>
|
<span class="tab" onclick="switchTab('sysinfo',event)">SysInfo</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="tab-exec">
|
<div class="card" id="tab-exec">
|
||||||
<div class="card-title">Execute Command (Gateway)</div>
|
<div class="card-title">Shell Command (local)</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="e.g. ls, whoami, df -h">
|
||||||
<input class="input" id="cmd" placeholder="Command (e.g. ls, whoami, dir)">
|
<button class="btn btn-blue" onclick="runExec()">Execute</button>
|
||||||
<button class="btn btn-blue" onclick="execCmd()">Execute</button>
|
|
||||||
<div class="log" id="log-exec"></div>
|
<div class="log" id="log-exec"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="tab-files" style="display:none">
|
<div class="card" id="tab-files" style="display:none">
|
||||||
<div class="card-title">List Files (Gateway)</div>
|
<div class="card-title">List Files (local)</div>
|
||||||
<input class="input" id="path" placeholder="Path (e.g. /sdcard, .)" value=".">
|
<input class="input" id="path" placeholder="/sdcard or ." value=".">
|
||||||
<button class="btn btn-blue" onclick="listFiles()">List</button>
|
<button class="btn btn-blue" onclick="runFiles()">List</button>
|
||||||
<div class="log" id="log-files"></div>
|
<div class="log" id="log-files"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="tab-sysinfo" style="display:none">
|
<div class="card" id="tab-sysinfo" style="display:none">
|
||||||
<div class="card-title">System Info (Local)</div>
|
<div class="card-title">Device Info (local)</div>
|
||||||
<button class="btn btn-blue" onclick="showSysInfo()">Refresh</button>
|
<button class="btn btn-blue" onclick="runSysinfo()">Refresh</button>
|
||||||
<div class="log" id="log-sysinfo"></div>
|
<div class="log" id="log-sysinfo"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function switchTab(t){
|
function switchTab(t,ev){
|
||||||
document.querySelectorAll('.tab').forEach(e=>e.classList.toggle('tab-on',false));
|
ev.target.classList.add('tab-on');
|
||||||
document.querySelectorAll('.card').forEach(e=>e.style.display='none');
|
['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';
|
document.getElementById('tab-'+t).style.display='block';
|
||||||
event.target.classList.toggle('tab-on',true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(id,msg){
|
function log(id,msg){
|
||||||
@@ -60,32 +60,25 @@ function log(id,msg){
|
|||||||
l.innerText+=msg+'\n';l.scrollTop=l.scrollHeight;
|
l.innerText+=msg+'\n';l.scrollTop=l.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function execCmd(){
|
function runExec(){
|
||||||
var gw=document.getElementById('gwUrl').value,c=document.getElementById('cmd').value;
|
var c=document.getElementById('cmd').value, id='log-exec';
|
||||||
var id='log-exec';document.getElementById(id).innerText='';
|
document.getElementById(id).innerText='';
|
||||||
log(id,'$ '+c);
|
log(id,'$ '+c);
|
||||||
fetch(gw+'/tools/exec',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({command:c})})
|
var r=Android.exec(c);
|
||||||
.then(r=>r.json()).then(d=>log(id,d.stdout||d.stderr||JSON.stringify(d)))
|
log(id,r||'(empty)');
|
||||||
.catch(e=>log(id,'ERR: '+e.message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function listFiles(){
|
function runFiles(){
|
||||||
var gw=document.getElementById('gwUrl').value,p=document.getElementById('path').value;
|
var p=document.getElementById('path').value, id='log-files';
|
||||||
var id='log-files';document.getElementById(id).innerText='';
|
document.getElementById(id).innerText='';
|
||||||
log(id,'ls '+p);
|
log(id,'ls '+p);
|
||||||
fetch(gw+'/tools/file_list',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p})})
|
var r=Android.listFiles(p);
|
||||||
.then(r=>r.json()).then(d=>{
|
log(id,r||'(empty)');
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSysInfo(){
|
function runSysinfo(){
|
||||||
var id='log-sysinfo';document.getElementById(id).innerText='';
|
var id='log-sysinfo';
|
||||||
log(id,'Platform: '+navigator.platform);
|
document.getElementById(id).innerText='';
|
||||||
log(id,'UserAgent: '+navigator.userAgent);
|
log(id,Android.sysinfo());
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
</script></body></html>
|
</script></body></html>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package dev.yuzu.gca
|
package dev.yuzu.gca
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import java.io.File
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -11,7 +15,44 @@ class MainActivity : AppCompatActivity() {
|
|||||||
settings.javaScriptEnabled = true
|
settings.javaScriptEnabled = true
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
settings.allowUniversalAccessFromFileURLs = true
|
settings.allowUniversalAccessFromFileURLs = true
|
||||||
|
addJavascriptInterface(LocalBridge(), "Android")
|
||||||
loadUrl("file:///android_asset/index.html")
|
loadUrl("file:///android_asset/index.html")
|
||||||
}.also { setContentView(it) }
|
}.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