소스 검색

feat: MCP plugin debugging — __eval + enhanced error reporting

- __eval js_code: runs JS in WebView, returns result via MCP event sync
- __eval_result events: displayed as mcp-result type in console
- Rhino error messages: include JS exception details and stack traces
- AppSession: PluginOpener adds evalJs callback
- evaluateJavascript bridge for direct WebView JS execution
- api://plugin resource documents all debug commands

Debug workflow:
1. evaluate('__open plugin_id') — open plugin for inspection
2. evaluate('__eval console.log(...)') — inject debug logging
3. execute_plugin('plugin_id') — test with Rhino error traces

Co-Authored-By: Claude <noreply@anthropic.com>
nonvioce 3 달 전
부모
커밋
1a9c600d74

+ 15 - 0
app/src/main/assets/console.js

@@ -2145,6 +2145,21 @@ class SymjaConsole {
                     if (pl) this.openPlugin(pl);
                     if (pl) this.openPlugin(pl);
                     return;
                     return;
                 }
                 }
+                // __eval 结果回传
+                if (ev.expression === '__eval_result') {
+                    try {
+                        var er = JSON.parse(ev.result);
+                        if (er.ok) {
+                            this.addOutput('__eval → ' + JSON.stringify(er.result), 'mcp-result');
+                        } else {
+                            this.addOutput('__eval error: ' + er.error, 'error');
+                        }
+                    } catch(e) {
+                        this.addOutput('__eval raw: ' + ev.result, 'mcp-result');
+                    }
+                    return;
+                }
+
                 // 调试命令:模拟画布点击 __tap x y (逻辑坐标)
                 // 调试命令:模拟画布点击 __tap x y (逻辑坐标)
                 if (ev.expression.startsWith('__tap ')) {
                 if (ev.expression.startsWith('__tap ')) {
                     var parts = ev.expression.split(/\s+/);
                     var parts = ev.expression.split(/\s+/);

+ 7 - 1
app/src/main/java/com/nonvioce/ijs/AppSession.java

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

+ 31 - 7
app/src/main/java/com/nonvioce/ijs/MainActivity.java

@@ -29,13 +29,37 @@ public class MainActivity extends AppCompatActivity {
         pluginManager = AppSession.getPluginManager(this);
         pluginManager = AppSession.getPluginManager(this);
 
 
         // MCP 插件调试桥
         // 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);
-            });
+        AppSession.setPluginOpener(new AppSession.PluginOpener() {
+            @Override
+            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);
+                });
+            }
+            @Override
+            public void evalJs(String jsCode) {
+                // 转义并包装为 evaluateJavascript 兼容的格式
+                String escaped = jsCode.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "");
+                binding.webView.post(() -> {
+                    binding.webView.evaluateJavascript(
+                        "(function(){\n" +
+                        "  try {\n" +
+                        "    var __r = eval('" + escaped + "');\n" +
+                        "    return JSON.stringify({ok:true,result:__r});\n" +
+                        "  } catch(e) {\n" +
+                        "    return JSON.stringify({ok:false,error:e.message||String(e)});\n" +
+                        "  }\n" +
+                        "})()",
+                        result -> {
+                            android.util.Log.d("IJS_DEBUG", "__eval result: " + result);
+                            AppSession.publishMcpEvent("__eval_result",
+                                result != null ? result : "null");
+                        });
+                });
+            }
         });
         });
 
 
         // 配置WebView
         // 配置WebView

+ 16 - 1
app/src/main/java/com/nonvioce/ijs/McpServer.java

@@ -437,6 +437,12 @@ public class McpServer extends NanoHTTPD {
             AppSession.requestOpenPlugin(pluginId);
             AppSession.requestOpenPlugin(pluginId);
             return "{\"opened\":\"" + pluginId + "\"}";
             return "{\"opened\":\"" + pluginId + "\"}";
         }
         }
+        // 调试命令:__eval js_code — 在 WebView 中执行 JS
+        if (expression.startsWith("__eval ")) {
+            String js = expression.substring(7);
+            AppSession.requestEvalJs(js);
+            return "{\"eval_sent\":true,\"note\":\"result arrives via MCP event sync (~1s delay)\"}";
+        }
         if (expression.startsWith("__tap ")) {
         if (expression.startsWith("__tap ")) {
             AppSession.publishMcpEvent(expression, "");
             AppSession.publishMcpEvent(expression, "");
             return "{\"tapped\":" + expression.substring(5).trim() + "}";
             return "{\"tapped\":" + expression.substring(5).trim() + "}";
@@ -688,7 +694,15 @@ public class McpServer extends NanoHTTPD {
             return output.isEmpty() ? "(plugin returned nothing)" : output;
             return output.isEmpty() ? "(plugin returned nothing)" : output;
 
 
         } catch (Exception e) {
         } catch (Exception e) {
-            return "Error executing plugin: " + e.getMessage();
+            String msg = e.getMessage();
+            // Rhino wraps JS errors — extract the actual JS error
+            if (msg != null && msg.contains("JavaScript exception")) {
+                msg = "JS: " + msg;
+            }
+            // Include stack trace for debugging
+            java.io.StringWriter sw = new java.io.StringWriter();
+            e.printStackTrace(new java.io.PrintWriter(sw));
+            return "Error: " + msg + "\n---\n" + sw.toString().substring(0, Math.min(500, sw.toString().length()));
         } finally {
         } finally {
             Context.exit();
             Context.exit();
         }
         }
@@ -921,6 +935,7 @@ public class McpServer extends NanoHTTPD {
             api.put("version", "2.0");
             api.put("version", "2.0");
             api.put("namespace", "IJS");
             api.put("namespace", "IJS");
             api.put("plugin_modes", "inline (dialog) or fullscreen (interactive panel)");
             api.put("plugin_modes", "inline (dialog) or fullscreen (interactive panel)");
+            api.put("debug_commands", "__open plugin_id (open fullscreen plugin), __eval js_code (run JS in WebView, result via event sync), __tap x y (simulate canvas touch)");
             api.put("libraries", "lodash 4.17.21, seedrandom 3.0.5, Matter.js 0.19.0");
             api.put("libraries", "lodash 4.17.21, seedrandom 3.0.5, Matter.js 0.19.0");
             JSONObject symja = new JSONObject();
             JSONObject symja = new JSONObject();
             symja.put("print", "(msg) - console output");
             symja.put("print", "(msg) - console output");