Skip to content

Commit 535e1d8

Browse files
pythonHuangclaude
andcommitted
feat: 流程设计器新增数据提取节点-输入可选流程入出参与中间变量,提取类型json(首个对象数组)/code(代码块)/keyword(关键字后到行尾)/between(起止标志)/length(偏移长度),输出可赋值变量出参入参,附完整帮助弹窗
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent e211792 commit 535e1d8

4 files changed

Lines changed: 406 additions & 12 deletions

File tree

‎Juggle.Domain/Engine/FlowEngine.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public async Task<FlowResult> ExecuteAsync(
171171
_redisConnStr ?? throw new InvalidOperationException("未配置 Redis(系统设置 → Redis 配置),无法执行 Redis 设置节点")),
172172
"KB_SEARCH" => new KbSearchNodeExecutor(
173173
_kbSearchFunc ?? throw new InvalidOperationException("流程引擎未接入知识库检索能力,无法执行知识库检索节点")),
174+
"DATA_EXTRACT" => new DataExtractNodeExecutor(),
174175
_ => throw new InvalidOperationException($"未知节点类型: {currentNode.ElementType}")
175176
};
176177

‎Juggle.Domain/Engine/FlowModels.cs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public class FlowNode
5353
[JsonPropertyName("kbSearchConfig")]
5454
public KbSearchConfig? KbSearchConfig { get; set; }
5555

56+
[JsonPropertyName("dataExtractConfig")]
57+
public DataExtractConfig? DataExtractConfig { get; set; }
58+
5659
[JsonPropertyName("redisSetConfig")]
5760
public RedisSetConfig? RedisSetConfig { get; set; }
5861

@@ -403,6 +406,46 @@ public class KbSearchConfig
403406
public string Output { get; set; } = "";
404407
}
405408

409+
/// <summary>数据提取节点配置(DATA_EXTRACT 节点)</summary>
410+
public class DataExtractConfig
411+
{
412+
/// <summary>输入变量(流程入参/中间变量/出参)</summary>
413+
[JsonPropertyName("input")]
414+
public string Input { get; set; } = "";
415+
416+
/// <summary>提取类型:json / code / keyword / between / length</summary>
417+
[JsonPropertyName("extractType")]
418+
public string ExtractType { get; set; } = "json";
419+
420+
/// <summary>关键字(keyword 类型)</summary>
421+
[JsonPropertyName("keyword")]
422+
public string Keyword { get; set; } = "";
423+
424+
/// <summary>开始标志(between 类型)</summary>
425+
[JsonPropertyName("startFlag")]
426+
public string StartFlag { get; set; } = "";
427+
428+
/// <summary>结束标志(between 类型,空=到结尾)</summary>
429+
[JsonPropertyName("endFlag")]
430+
public string EndFlag { get; set; } = "";
431+
432+
/// <summary>偏移(length 类型)</summary>
433+
[JsonPropertyName("offset")]
434+
public int Offset { get; set; }
435+
436+
/// <summary>长度(length 类型,0=到结尾)</summary>
437+
[JsonPropertyName("length")]
438+
public int Length { get; set; }
439+
440+
/// <summary>输出目标名</summary>
441+
[JsonPropertyName("output")]
442+
public string Output { get; set; } = "";
443+
444+
/// <summary>输出目标类型:VARIABLE / OUTPUT / INPUT</summary>
445+
[JsonPropertyName("outputTargetType")]
446+
public string OutputTargetType { get; set; } = "VARIABLE";
447+
}
448+
406449
/// <summary>MySQL 节点配置(MYSQL 节点)</summary>
407450
public class MysqlConfig
408451
{
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
4+
namespace Juggle.Domain.Engine.NodeExecutors;
5+
6+
/// <summary>
7+
/// 数据提取节点:从输入文本中提取内容。
8+
/// 提取类型:json(首个 JSON 对象/数组)、code(代码块)、keyword(关键字后内容)、
9+
/// between(开始/结束标志之间)、length(按偏移与长度截取)。
10+
/// </summary>
11+
public class DataExtractNodeExecutor : INodeExecutor
12+
{
13+
public Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
14+
{
15+
var cfg = node.DataExtractConfig
16+
?? throw new InvalidOperationException($"数据提取节点 [{node.Key}] 未配置 dataExtractConfig。");
17+
18+
var raw = string.IsNullOrWhiteSpace(cfg.Input) ? "" : context.GetVariable(cfg.Input)?.ToString() ?? "";
19+
var type = (cfg.ExtractType ?? "json").ToLower();
20+
21+
object? result = type switch
22+
{
23+
"json" => ExtractJson(raw),
24+
"code" => ExtractCode(raw),
25+
"keyword" => ExtractAfterKeyword(raw, cfg.Keyword ?? ""),
26+
"between" => ExtractBetween(raw, cfg.StartFlag ?? "", cfg.EndFlag ?? ""),
27+
"length" => ExtractLength(raw, cfg.Offset, cfg.Length),
28+
_ => raw
29+
};
30+
31+
if (!string.IsNullOrWhiteSpace(cfg.Output))
32+
{
33+
switch ((cfg.OutputTargetType ?? "VARIABLE").ToUpper())
34+
{
35+
case "OUTPUT":
36+
context.SetOutputParameter(cfg.Output, result);
37+
break;
38+
default:
39+
context.SetVariable(cfg.Output, result);
40+
break;
41+
}
42+
}
43+
44+
return Task.FromResult(node.Outgoings.FirstOrDefault());
45+
}
46+
47+
/// <summary>提取首个 JSON 对象/数组(返回解析对象,失败返回原文)。</summary>
48+
public static object? ExtractJson(string raw)
49+
{
50+
var start = raw.IndexOfAny(new[] { '{', '[' });
51+
if (start < 0) return raw;
52+
var end = FindBalancedEnd(raw, start);
53+
if (end < 0) return raw;
54+
var json = raw.Substring(start, end - start + 1);
55+
try
56+
{
57+
using var doc = JsonDocument.Parse(json);
58+
return FileParseNodeExecutor.CloneToNative(doc.RootElement);
59+
}
60+
catch { return json; }
61+
}
62+
63+
/// <summary>从 { 或 [ 开始找平衡闭合位置。</summary>
64+
private static int FindBalancedEnd(string s, int start)
65+
{
66+
var openers = new Stack<char>();
67+
for (int i = start; i < s.Length; i++)
68+
{
69+
var ch = s[i];
70+
if (ch == '{' || ch == '[')
71+
{
72+
openers.Push(ch);
73+
}
74+
else if (ch == '}' || ch == ']')
75+
{
76+
if (openers.Count == 0) return -1;
77+
var opener = openers.Pop();
78+
if ((ch == '}' && opener != '{') || (ch == ']' && opener != '[')) return -1;
79+
if (openers.Count == 0) return i;
80+
}
81+
}
82+
return -1;
83+
}
84+
85+
/// <summary>提取代码块内容(```lang ... ```)。</summary>
86+
public static string ExtractCode(string raw)
87+
{
88+
var fenceIdx = raw.IndexOf("```", StringComparison.Ordinal);
89+
if (fenceIdx < 0) return raw;
90+
var contentStart = raw.IndexOf('\n', fenceIdx);
91+
if (contentStart < 0) return raw;
92+
var endIdx = raw.IndexOf("```", contentStart + 1, StringComparison.Ordinal);
93+
if (endIdx < 0) return raw.Substring(contentStart + 1).Trim();
94+
return raw.Substring(contentStart + 1, endIdx - contentStart - 1).Trim();
95+
}
96+
97+
/// <summary>提取关键字之后的内容(跳过 : := 空格等分隔符,取到行尾)。</summary>
98+
public static string ExtractAfterKeyword(string raw, string keyword)
99+
{
100+
if (string.IsNullOrEmpty(keyword)) return raw;
101+
var idx = raw.IndexOf(keyword, StringComparison.Ordinal);
102+
if (idx < 0) return "";
103+
var pos = idx + keyword.Length;
104+
while (pos < raw.Length && (raw[pos] == ':' || raw[pos] == ':' || raw[pos] == '=' || raw[pos] == ' ' || raw[pos] == '\t'))
105+
pos++;
106+
var lineEnd = raw.IndexOf('\n', pos);
107+
return lineEnd < 0 ? raw.Substring(pos).Trim() : raw.Substring(pos, lineEnd - pos).Trim();
108+
}
109+
110+
/// <summary>提取开始标志与结束标志之间的内容(首次出现)。</summary>
111+
public static string ExtractBetween(string raw, string startFlag, string endFlag)
112+
{
113+
if (string.IsNullOrEmpty(startFlag)) return raw;
114+
var sIdx = raw.IndexOf(startFlag, StringComparison.Ordinal);
115+
if (sIdx < 0) return "";
116+
var start = sIdx + startFlag.Length;
117+
if (string.IsNullOrEmpty(endFlag)) return raw.Substring(start);
118+
var eIdx = raw.IndexOf(endFlag, start, StringComparison.Ordinal);
119+
return eIdx < 0 ? raw.Substring(start) : raw.Substring(start, eIdx - start);
120+
}
121+
122+
/// <summary>按偏移与长度截取。</summary>
123+
public static string ExtractLength(string raw, int offset, int length)
124+
{
125+
var off = Math.Max(0, offset);
126+
if (off >= raw.Length) return "";
127+
var len = length > 0 ? length : raw.Length - off;
128+
return raw.Substring(off, Math.Min(len, raw.Length - off));
129+
}
130+
}

0 commit comments

Comments
 (0)