Skip to content

Commit fbe40fb

Browse files
💾 Feat(BlockScript/Blueprint): 实现插件函数全链路解析与类型化执行
管线层(BS→BP): - ExprUtils 新增 GetFullMethodName() 保留完整点分路径 - FormattedStatement 新增 FullFunctionName 属性 - ScriptFormatter 在 FormatInvocation 和 ExpandExpression 中传递完整函数名 - NodeBuilder 解析 PluginName/FunctionName 到 CallNode - ExpandExpression 拆分嵌套调用时保留 FullFunctionName 执行层(BS Runtime): - BlockScriptExecutor 新增 PluginCallRewriter (Roslyn AST级转换) 将 A.B.C.Func(args) → PluginCall("A.B.C", "Func", args) - BlockScriptExecutionGlobals 新增 PluginCall 统一入口 - RealPluginManager 新增 CallAuto() 自动分发策略: void → fire-and-forget, 非void → 反射调用 Call<T>() - RealPluginManager 新增 GetFunctionReturnType() 查询插件函数返回类型 - ParseResult<object?> 增加非JSON纯文本容错 - CoreServiceCollectionExtensions/WorkflowScriptService 注入 PluginManager 导出层(BP→BS): - CallNodeExportStrategy 已正确处理 PluginName 生成完整调用路径
1 parent 7ff97b4 commit fbe40fb

10 files changed

Lines changed: 337 additions & 14 deletions

File tree

KitX Clients/KitX Core/KitX.Core/DI/CoreServiceCollectionExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ public static IServiceCollection AddCoreServices(this IServiceCollection service
188188
services.AddSingleton<IBlockScriptExecutor, KitX.Core.Workflow.BlockScripting.BlockScriptExecutor>(provider =>
189189
{
190190
var service = new KitX.Core.Workflow.BlockScripting.BlockScriptExecutor();
191+
// Wire up plugin manager if PluginsServer is available
192+
try
193+
{
194+
var pluginManager = new KitX.Core.Workflow.RealPluginManager(
195+
KitX.Core.Device.PluginsServer.Instance);
196+
service.SetPluginManager(pluginManager);
197+
}
198+
catch (Exception ex)
199+
{
200+
Log.Warning(ex, "Could not initialize BlockScriptExecutor with plugin manager");
201+
}
191202
return service;
192203
});
193204

KitX Clients/KitX Core/KitX.Core/Workflow/BlockScripting/BlockScriptExecutionGlobals.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Threading;
4+
using Kscript.CSharp.Parser.Core;
5+
using Kscript.CSharp.Parser.Models;
46
using KitX.Core.Workflow;
7+
using Serilog;
58

69
namespace KitX.Core.Workflow.BlockScripting;
710

@@ -13,6 +16,7 @@ public class BlockScriptExecutionGlobals
1316
private readonly BlockScopeManager _scopeManager;
1417
private readonly List<string> _output;
1518
private readonly Dictionary<string, object?> _variables = new();
19+
private readonly IPluginManager? _pluginManager;
1620

1721
/// <summary>
1822
/// NextBlock 内置变量 - 设置后执行器会跳转到指定块
@@ -29,6 +33,15 @@ public BlockScriptExecutionGlobals(BlockScopeManager scopeManager, List<string>
2933
_output = output;
3034
}
3135

36+
/// <summary>
37+
/// Creates script globals with plugin manager support
38+
/// </summary>
39+
public BlockScriptExecutionGlobals(BlockScopeManager scopeManager, List<string> output,
40+
IPluginManager? pluginManager) : this(scopeManager, output)
41+
{
42+
_pluginManager = pluginManager;
43+
}
44+
3245
/// <summary>
3346
/// Gets a variable value - called by CSharpScript when accessing unknown properties
3447
/// Returns dynamic to allow implicit conversion to target variable types
@@ -127,4 +140,40 @@ public void Pause(int milliseconds)
127140
NextBlock = $"{parentBlockName}_Loop";
128141
return NextBlock;
129142
}
143+
144+
/// <summary>
145+
/// 调用插件函数。所有分发策略(类型化调用、fire-and-forget vs 同步等待)
146+
/// 由 RealPluginManager.CallAuto() 内部自动完成,调用方无需关心。
147+
/// </summary>
148+
public object? PluginCall(string pluginName, string methodName, params object?[] args)
149+
{
150+
if (_pluginManager == null)
151+
{
152+
Log.Warning("[BlockScriptGlobals] PluginCall: no plugin manager available, " +
153+
"cannot call {PluginName}.{MethodName}", pluginName, methodName);
154+
return null;
155+
}
156+
157+
var callInfo = new PluginCallInfo
158+
{
159+
PluginName = pluginName,
160+
MethodName = methodName,
161+
Parameters = args ?? Array.Empty<object?>()
162+
};
163+
164+
try
165+
{
166+
if (_pluginManager is RealPluginManager realManager)
167+
return realManager.CallAuto(callInfo);
168+
169+
// Fallback for other IPluginManager implementations
170+
return _pluginManager.Call<object?>(callInfo);
171+
}
172+
catch (Exception ex)
173+
{
174+
Log.Error(ex, "[BlockScriptGlobals] PluginCall failed: {PluginName}.{MethodName}",
175+
pluginName, methodName);
176+
return null;
177+
}
178+
}
130179
}

KitX Clients/KitX Core/KitX.Core/Workflow/BlockScripting/BlockScriptExecutor.cs

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
using System.Reflection;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Kscript.CSharp.Parser.Core;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
812
using Microsoft.CodeAnalysis.CSharp.Scripting;
913
using Microsoft.CodeAnalysis.Scripting;
1014
using KitX.Core.Contract.Workflow;
@@ -24,6 +28,7 @@ public class BlockScriptExecutor : IBlockScriptExecutor
2428
private BlockScript? _currentScript;
2529
private List<string> _output = new();
2630
private BlockScriptExecutionGlobals? _globals;
31+
private IPluginManager? _pluginManager;
2732

2833
/// <summary>
2934
/// Creates a new block script executor
@@ -41,6 +46,22 @@ public BlockScriptExecutor(BlockScopeManager scopeManager)
4146
_scopeManager = scopeManager;
4247
}
4348

49+
/// <summary>
50+
/// Creates a new block script executor with plugin manager support
51+
/// </summary>
52+
public BlockScriptExecutor(IPluginManager? pluginManager) : this()
53+
{
54+
_pluginManager = pluginManager;
55+
}
56+
57+
/// <summary>
58+
/// Sets the plugin manager for plugin function calls during execution.
59+
/// </summary>
60+
public void SetPluginManager(IPluginManager? pluginManager)
61+
{
62+
_pluginManager = pluginManager;
63+
}
64+
4465
/// <summary>
4566
/// Executes a block script
4667
/// </summary>
@@ -488,6 +509,9 @@ private async Task ExecuteExpressionAsync(
488509
{
489510
try
490511
{
512+
// Pre-process: rewrite dotted plugin calls (e.g. "Plugin.Func()") to PluginCall(...)
513+
expression = PreProcessPluginCalls(expression);
514+
491515
// Build a complete statement for execution
492516
var code = WrapExpressionAsStatement(expression);
493517

@@ -524,7 +548,7 @@ private async Task ExecuteExpressionAsync(
524548
else
525549
{
526550
// First evaluation: create initial script state using RunAsync to get ScriptState
527-
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output);
551+
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output, _pluginManager);
528552
var globals = _globals;
529553
_scriptState = await CSharpScript.RunAsync(
530554
fullCode,
@@ -560,6 +584,106 @@ private string WrapExpressionAsStatement(string expression)
560584
return expression + ";";
561585
}
562586

587+
/// <summary>
588+
/// Preprocesses an expression to rewrite dotted plugin calls into PluginCall() invocations.
589+
/// Uses Roslyn SyntaxRewriter for safe, AST-level transformation that correctly handles
590+
/// nested parentheses, complex expressions, and all edge cases.
591+
/// Transforms: TestPlugin.WPF.Core.HelloKitX(arg1, arg2)
592+
/// Into: PluginCall("TestPlugin.WPF.Core", "HelloKitX", arg1, arg2)
593+
/// Transforms: TestPlugin.WPF.Core.HelloKitX()
594+
/// Into: PluginCall("TestPlugin.WPF.Core", "HelloKitX")
595+
/// </summary>
596+
private static string PreProcessPluginCalls(string expression)
597+
{
598+
try
599+
{
600+
var wrappedCode = "_ = " + expression + ";";
601+
var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode);
602+
var root = syntaxTree.GetCompilationUnitRoot();
603+
604+
var rewriter = new PluginCallRewriter();
605+
var rewritten = rewriter.Visit(root);
606+
607+
// Extract the expression back from "_ = ...;"
608+
if (rewritten is CompilationUnitSyntax cu
609+
&& cu.Members.FirstOrDefault() is GlobalStatementSyntax gs
610+
&& gs.Statement is ExpressionStatementSyntax ess
611+
&& ess.Expression is AssignmentExpressionSyntax aes)
612+
{
613+
var result = aes.Right.ToString();
614+
if (result != expression)
615+
{
616+
Log.Debug("[BlockScriptExecutor] PreProcessPluginCalls: '{Original}' → '{Result}'",
617+
expression, result);
618+
}
619+
return result;
620+
}
621+
622+
Log.Warning("[BlockScriptExecutor] PreProcessPluginCalls: extraction failed for '{Expression}', " +
623+
"rewritten type={Type}", expression, rewritten?.GetType().Name);
624+
return expression;
625+
}
626+
catch (Exception ex)
627+
{
628+
Log.Warning(ex, "[BlockScriptExecutor] PreProcessPluginCalls: exception for '{Expression}'",
629+
expression);
630+
return expression;
631+
}
632+
}
633+
634+
/// <summary>
635+
/// Roslyn SyntaxRewriter that transforms dotted member-access invocations
636+
/// (e.g. TestPlugin.WPF.Core.HelloKitX()) into PluginCall("...", "...", args) calls.
637+
/// Operates at the AST level, preserving all expression structure and handling nesting.
638+
/// </summary>
639+
private class PluginCallRewriter : CSharpSyntaxRewriter
640+
{
641+
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
642+
{
643+
// First, recursively rewrite any nested invocations in arguments
644+
node = (InvocationExpressionSyntax)base.VisitInvocationExpression(node)!;
645+
646+
// Only rewrite member-access invocations (e.g. A.B.C.Method())
647+
if (node.Expression is not MemberAccessExpressionSyntax member)
648+
return node;
649+
650+
// Only rewrite when the expression has dots (plugin namespace path)
651+
var fullExpression = member.Expression.ToString();
652+
if (!fullExpression.Contains('.'))
653+
return node;
654+
655+
var pluginName = fullExpression;
656+
var funcName = member.Name.Identifier.Text;
657+
658+
Log.Debug("[PluginCallRewriter] Rewriting: {Plugin}.{Method}() → PluginCall()",
659+
pluginName, funcName);
660+
661+
// Build: PluginCall("pluginName", "funcName" [, existingArgs])
662+
// 所有分发策略(类型化调用、f-a-f vs 同步等待)由 Manager 内部自动处理
663+
var args = new List<ArgumentSyntax>
664+
{
665+
SyntaxFactory.Argument(
666+
SyntaxFactory.LiteralExpression(
667+
SyntaxKind.StringLiteralExpression,
668+
SyntaxFactory.Literal(pluginName))),
669+
SyntaxFactory.Argument(
670+
SyntaxFactory.LiteralExpression(
671+
SyntaxKind.StringLiteralExpression,
672+
SyntaxFactory.Literal(funcName)))
673+
};
674+
675+
// Append original arguments
676+
args.AddRange(node.ArgumentList.Arguments);
677+
678+
var newArgsList = SyntaxFactory.SeparatedList(args);
679+
var newInvocation = SyntaxFactory.InvocationExpression(
680+
SyntaxFactory.IdentifierName("PluginCall"),
681+
SyntaxFactory.ArgumentList(newArgsList));
682+
683+
return newInvocation;
684+
}
685+
}
686+
563687
/// <summary>
564688
/// Builds helper function code from a list of helper functions
565689
/// </summary>
@@ -644,7 +768,7 @@ private async Task InitializeScriptSessionAsync(CancellationToken cancellationTo
644768
if (string.IsNullOrWhiteSpace(initCode))
645769
{
646770
// No initialization needed, just do a simple first evaluation to establish session
647-
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output);
771+
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output, _pluginManager);
648772
_scriptState = await CSharpScript.RunAsync(
649773
"0", // Simple expression to establish session
650774
ScriptOptions.Default
@@ -659,7 +783,7 @@ private async Task InitializeScriptSessionAsync(CancellationToken cancellationTo
659783

660784
try
661785
{
662-
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output);
786+
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output, _pluginManager);
663787
_scriptState = await CSharpScript.RunAsync(
664788
initCode,
665789
ScriptOptions.Default
@@ -672,7 +796,7 @@ private async Task InitializeScriptSessionAsync(CancellationToken cancellationTo
672796
{
673797
Log.Warning(ex, "[BlockScriptExecutor] Initialization script failed, will try without it");
674798
// Fall back to simple session establishment
675-
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output);
799+
_globals = new BlockScriptExecutionGlobals(_scopeManager, _output, _pluginManager);
676800
_scriptState = await CSharpScript.RunAsync(
677801
"0",
678802
ScriptOptions.Default

KitX Clients/KitX Core/KitX.Core/Workflow/Blueprint/Pipeline/ExprUtils.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public static (ExpressionSyntax? rightExpr, string? assignedVar)? ParseStatement
8282
catch { return (null, null); }
8383
}
8484

85-
/// <summary>Extracts method name from an invocation expression.</summary>
85+
/// <summary>Extracts the short method name from an invocation expression.</summary>
8686
public static string GetMethodName(InvocationExpressionSyntax invoke)
8787
{
8888
if (invoke.Expression is IdentifierNameSyntax id) return id.Identifier.Text;
@@ -91,6 +91,20 @@ public static string GetMethodName(InvocationExpressionSyntax invoke)
9191
return string.Empty;
9292
}
9393

94+
/// <summary>
95+
/// Extracts the full dotted method path from an invocation expression.
96+
/// For "TestPlugin.WPF.Core.HelloKitX()" returns "TestPlugin.WPF.Core.HelloKitX".
97+
/// For simple calls like "Get(...)" returns just "Get".
98+
/// </summary>
99+
public static string GetFullMethodName(InvocationExpressionSyntax invoke)
100+
{
101+
if (invoke.Expression is IdentifierNameSyntax id) return id.Identifier.Text;
102+
if (invoke.Expression is GenericNameSyntax generic) return generic.Identifier.Text;
103+
if (invoke.Expression is MemberAccessExpressionSyntax member)
104+
return member.Expression.ToString() + "." + member.Name.Identifier.Text;
105+
return string.Empty;
106+
}
107+
94108
/// <summary>Checks if a token is a simple variable reference.</summary>
95109
public static bool IsVariableReference(string token)
96110
{

KitX Clients/KitX Core/KitX.Core/Workflow/Blueprint/Pipeline/FormattedBlockScript.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,16 @@ public class FormattedStatement
5757
// --- For function calls ---
5858
/// <summary>
5959
/// Function name (e.g. "HelperFuncCompare", "Get", "Set", "Print").
60+
/// For plugin calls, this is the short name (e.g. "HelloKitX").
6061
/// </summary>
6162
public string? FunctionName { get; set; }
6263

64+
/// <summary>
65+
/// Full dotted method path for plugin/external calls (e.g. "TestPlugin.WPF.Core.HelloKitX").
66+
/// Null for built-in and helper functions.
67+
/// </summary>
68+
public string? FullFunctionName { get; set; }
69+
6370
/// <summary>
6471
/// Raw argument strings after expansion (no nested calls).
6572
/// Each argument is either a literal, a PubVar name, a ConstBlock variable name, or Get("varName").

KitX Clients/KitX Core/KitX.Core/Workflow/Blueprint/Pipeline/NodeBuilder.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,19 @@ or FormattedStatementKind.LoopBodyEnd
227227
else
228228
{
229229
var callNode = (CallNode)_registry.Create(BlueprintNodeType.Call);
230-
callNode.FunctionName = stmt.FunctionName!;
230+
231+
// Parse plugin name from full dotted method name (e.g. "TestPlugin.WPF.Core.HelloKitX")
232+
if (!string.IsNullOrEmpty(stmt.FullFunctionName) && stmt.FullFunctionName.Contains('.'))
233+
{
234+
var lastDot = stmt.FullFunctionName.LastIndexOf('.');
235+
callNode.PluginName = stmt.FullFunctionName.Substring(0, lastDot);
236+
callNode.FunctionName = stmt.FullFunctionName.Substring(lastDot + 1);
237+
}
238+
else
239+
{
240+
callNode.FunctionName = stmt.FunctionName!;
241+
}
242+
231243
mainNode = callNode;
232244
}
233245

0 commit comments

Comments
 (0)