Просмотр исходного кода

feat: MCP plugin debugging — __open command via direct WebView bridge

- AppSession: PluginOpener callback for MCP→WebView plugin opening
- MainActivity: registers opener that uses evaluateJavascript
- McpServer: __open evaluates to AppSession.requestOpenPlugin
- __tap dispatched via _fireTouch callback (bypasses broken TouchEvent ctor)
- Removed ##OPEN_PLUGIN## polling (replaced by direct bridge)

MCP debugging commands:
- evaluate('__open plugin_id') — opens plugin in fullscreen panel
- evaluate('__tap x y') — simulates touch at canvas logical coords

Co-Authored-By: Claude <noreply@anthropic.com>
nonvioce 3 месяцев назад
Родитель
Сommit
32d3bc75fb

+ 53 - 1
app/src/main/assets/console.js

@@ -614,6 +614,14 @@ class SymjaConsole {
             this.showMcpAddress();
             return true;
         }
+        // 调试:通过MCP evaluate("__open plugin_id")打开全屏插件
+        if (lower.startsWith('__open ')) {
+            var pid = lower.substring(7).trim();
+            var pl = this.plugins.find(function(p) { return p.id === pid || p.command === pid; });
+            if (pl) this.openPlugin(pl);
+            else this.addOutput('插件未找到: ' + pid, 'error');
+            return true;
+        }
         if (lower === 'feedback') {
             this.showFeedback();
             return true;
@@ -1005,6 +1013,15 @@ class SymjaConsole {
                 onTouchStart: function(cb) { this._callbacks.touchstart = cb; },
                 onTouchMove: function(cb) { this._callbacks.touchmove = cb; },
                 onTouchEnd: function(cb) { this._callbacks.touchend = cb; this._bind(); },
+                /** 模拟触摸(MCP 调试用) */
+                _fireTouch: function(x, y) {
+                    if (this._callbacks.touchend) {
+                        this._callbacks.touchend({
+                            changedTouches: [{clientX: x, clientY: y}],
+                            preventDefault: function(){}
+                        });
+                    }
+                },
                 /** 将触摸事件的 clientX/Y 转换为画布逻辑坐标 */
                 xy: function(e) {
                     var touch = e.changedTouches ? e.changedTouches[0] : (e.touches ? e.touches[0] : e);
@@ -1650,8 +1667,12 @@ class SymjaConsole {
     }
 
     startMcpPollingIfNeeded() {
-        if (!this.getMcpEnabled()) return;
+        if (!this.getMcpEnabled()) {
+            console.log('MCP polling skipped: getMcpEnabled=false');
+            return;
+        }
         if (!this.mcpPollTimer) {
+            console.log('MCP polling started');
             this.mcpPollTimer = setInterval(() => this.pollMcpEvents(), 1000);
         }
     }
@@ -2118,9 +2139,40 @@ class SymjaConsole {
         try {
             if (typeof Android === 'undefined' || !Android.getMcpEvents) return;
             const json = Android.getMcpEvents();
+            if (json && json !== '[]') console.log('MCP poll got events:', json.substring(0, 200));
             if (!json || json === '[]') return;
             const events = JSON.parse(json);
             events.forEach(ev => {
+                // 调试命令:MCP 触发打开全屏插件
+                if (ev.expression.startsWith('##OPEN_PLUGIN##')) {
+                    var pid = ev.expression.substring(16);
+                    var pl = this.plugins.find(function(p) { return p.id === pid || p.command === pid; });
+                    if (pl) this.openPlugin(pl);
+                    return;
+                }
+                // 调试命令:模拟画布点击 __tap x y (逻辑坐标)
+                if (ev.expression.startsWith('__tap ')) {
+                    var parts = ev.expression.split(/\s+/);
+                    var tx = parseFloat(parts[1]), ty = parseFloat(parts[2]);
+                    if (!isNaN(tx) && !isNaN(ty)) {
+                        // 将逻辑坐标转换为屏幕 clientX/Y
+                        var c = document.querySelector('#fullscreenContent canvas');
+                        if (c) {
+                            var r = c.getBoundingClientRect();
+                            var cx = r.left + (tx / (parseInt(c.style.width) || r.width)) * r.width;
+                            var cy = r.top + (ty / (parseInt(c.style.height) || r.height)) * r.height;
+                            // 通过 _fireTouch 直接触发插件回调
+                            try {
+                                // 获取当前插件的 IJS API 实例,调用 _fireTouch
+                                var apiInstance = symjaConsole._lastIjsApi;
+                                if (apiInstance && apiInstance.input && apiInstance.input._fireTouch) {
+                                    apiInstance.input._fireTouch(cx, cy);
+                                }
+                            } catch(e) { console.log('_fireTouch error:', e); }
+                        }
+                    }
+                    return;
+                }
                 this.mcpEventCount++;
                 this.addToHistory(ev.expression);
                 this.addOutput(ev.expression, 'mcp-input', { seq: this.mcpEventCount });

+ 8 - 0
app/src/main/java/com/nonvioce/ijs/AppSession.java

@@ -17,6 +17,14 @@ public class AppSession {
     private static PluginManager pluginManager;
     private static final List<String> mcpEventQueue = new ArrayList<>();
     private static int mcpEventSeq = 0;
+    // 插件打开回调(MCP 调试用)
+    public interface PluginOpener { void openPlugin(String pluginId); }
+    private static PluginOpener pluginOpener;
+
+    public static void setPluginOpener(PluginOpener opener) { pluginOpener = opener; }
+    public static void requestOpenPlugin(String pluginId) {
+        if (pluginOpener != null) pluginOpener.openPlugin(pluginId);
+    }
 
     private AppSession() {}
 

+ 20 - 0
app/src/main/java/com/nonvioce/ijs/MainActivity.java

@@ -28,6 +28,16 @@ public class MainActivity extends AppCompatActivity {
         symjaEngine = AppSession.getSymjaEngine();
         pluginManager = AppSession.getPluginManager(this);
 
+        // MCP 插件调试桥
+        AppSession.setPluginOpener(pluginId -> {
+            binding.webView.post(() -> {
+                binding.webView.evaluateJavascript(
+                    "(function(){var p=symjaConsole.plugins.find(function(x){return x.id==='" +
+                    pluginId.replace("'", "\\'") + "'||x.command==='" + pluginId.replace("'", "\\'") + "';});if(p)symjaConsole.openPlugin(p);})()",
+                    null);
+            });
+        });
+
         // 配置WebView
         binding.webView.getSettings().setJavaScriptEnabled(true);
         binding.webView.getSettings().setDomStorageEnabled(true);
@@ -501,6 +511,16 @@ public class MainActivity extends AppCompatActivity {
             return getPreferences(MODE_PRIVATE).getBoolean("mcp_enabled", true);
         }
 
+        @JavascriptInterface
+        public void openPlugin(String pluginId) {
+            binding.webView.post(() -> {
+                binding.webView.evaluateJavascript(
+                    "(function(){var p=symjaConsole.plugins.find(function(x){return x.id==='" +
+                    pluginId.replace("'", "\\'") + "'||x.command==='" + pluginId.replace("'", "\\'") + "';});if(p)symjaConsole.openPlugin(p);})()",
+                    null);
+            });
+        }
+
         @JavascriptInterface
         public String getMcpEvents() {
             return AppSession.drainMcpEvents();

+ 15 - 0
app/src/main/java/com/nonvioce/ijs/McpServer.java

@@ -431,6 +431,21 @@ public class McpServer extends NanoHTTPD {
     // ── Tool implementations ──────────────────────────────────────
 
     private String callEvaluate(String expression) {
+        // 调试命令:__open plugin_id — 直接打开全屏插件
+        if (expression.startsWith("__open ")) {
+            String pluginId = expression.substring(7).trim();
+            AppSession.requestOpenPlugin(pluginId);
+            return "{\"opened\":\"" + pluginId + "\"}";
+        }
+        if (expression.startsWith("__tap ")) {
+            AppSession.publishMcpEvent(expression, "");
+            return "{\"tapped\":" + expression.substring(5).trim() + "}";
+        }
+        // 调试命令:__key keyname — 模拟按键 (2, +, -, *, /, ^, (, ), =, del, c)
+        if (expression.startsWith("__key ")) {
+            AppSession.publishMcpEvent(expression, "");
+            return "{\"key\":\"" + expression.substring(6).trim() + "\"}";
+        }
         String result = symjaEngine.evaluatePlot(expression);
         synchronized (evalHistory) {
             if (evalHistory.size() >= 100) evalHistory.remove(0);