Skip to content

Commit 2c4e9c7

Browse files
pythonHuangclaude
andcommitted
feat: 流程设计器新增大模型节点-属性含输入变量/系统提示词/输出变量,执行时调用已配置的大模型(OpenAI兼容供应商)把输入作为用户消息、回复写入输出变量,未配置供应商或缺少配置时给出明确报错,附完整帮助弹窗
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 1960f12 commit 2c4e9c7

5 files changed

Lines changed: 120 additions & 6 deletions

File tree

‎Juggle.Application/Services/Flow/FlowExecutionService.cs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text.Json;
22
using Juggle.Application.Services;
3+
using Juggle.Application.Services.Impl;
34
using Juggle.Domain.Engine;
45
using Juggle.Domain.Engine.NodeExecutors;
56
using Juggle.Domain.Entities;
@@ -17,14 +18,22 @@ public class FlowExecutionService
1718
private readonly JuggleDbContext _db;
1819
private readonly IHttpClientFactory _httpClientFactory;
1920
private readonly ITenantAccessor? _tenant;
21+
private readonly AiService _aiService;
2022

21-
public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFactory, ITenantAccessor? tenant = null)
23+
public FlowExecutionService(JuggleDbContext db, IHttpClientFactory httpClientFactory, AiService aiService, ITenantAccessor? tenant = null)
2224
{
2325
_db = db;
2426
_httpClientFactory = httpClientFactory;
2527
_tenant = tenant;
28+
_aiService = aiService;
2629
}
2730

31+
/// <summary>构建引擎(注入大模型对话函数,供 AI 节点使用)。</summary>
32+
private FlowEngine BuildEngine(Dictionary<string, DataSourceInfo> dsInfos,
33+
Dictionary<string, string?> staticVars, Func<string, Task<string?>> flowContentLoader)
34+
=> new FlowEngine(_httpClientFactory, dsInfos, staticVars, flowContentLoader,
35+
aiChatFunc: (systemPrompt, userInput) => _aiService.ChatAsync(systemPrompt, userInput));
36+
2837
// ────────────────────────────────────────────────────────────────
2938
// 数据源
3039
// ────────────────────────────────────────────────────────────────
@@ -193,7 +202,7 @@ public async Task<FlowResult> RunAsync(
193202
return ver?.FlowContent;
194203
}
195204

196-
var engine = new FlowEngine(_httpClientFactory, dsInfos, staticVars, FlowContentLoader);
205+
var engine = BuildEngine(dsInfos, staticVars, FlowContentLoader);
197206

198207
var inputJson = JsonSerializer.Serialize(inputParams);
199208
var startTime = DateTime.Now;
@@ -363,7 +372,7 @@ public async Task RunAsyncWithLog(
363372
return ver?.FlowContent;
364373
}
365374

366-
var engine = new FlowEngine(_httpClientFactory, dsInfos, staticVars, FlowContentLoader);
375+
var engine = BuildEngine(dsInfos, staticVars, FlowContentLoader);
367376
var startTime = DateTime.Now;
368377
FlowResult result;
369378
try

‎Juggle.Domain/Engine/FlowEngine.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ public class FlowEngine
1717
private readonly Dictionary<string, string?> _staticVarSnapshot;
1818
/// <summary>根据 flowKey 加载最新已发布流程内容(供 SUB_FLOW 节点调用),可为 null(不支持子流程)</summary>
1919
private readonly Func<string, Task<string?>>? _flowContentLoader;
20+
/// <summary>大模型对话函数(systemPrompt, userInput → reply),供 AI 节点调用,可为 null(未接入大模型)</summary>
21+
private readonly Func<string, string, Task<string>>? _aiChatFunc;
2022

2123
public FlowEngine(IHttpClientFactory httpClientFactory,
2224
Dictionary<string, DataSourceInfo>? dataSources = null,
2325
Dictionary<string, string?>? staticVariables = null,
24-
Func<string, Task<string?>>? flowContentLoader = null)
26+
Func<string, Task<string?>>? flowContentLoader = null,
27+
Func<string, string, Task<string>>? aiChatFunc = null)
2528
{
2629
_httpClientFactory = httpClientFactory;
2730
_dataSources = dataSources ?? new();
2831
_staticVarSnapshot = staticVariables ?? new(StringComparer.OrdinalIgnoreCase);
2932
_flowContentLoader = flowContentLoader;
33+
_aiChatFunc = aiChatFunc;
3034
}
3135

3236
public async Task<FlowResult> ExecuteAsync(
@@ -148,6 +152,8 @@ public async Task<FlowResult> ExecuteAsync(
148152
"PARALLEL" => new ParallelNodeExecutor(),
149153
"NOTIFY" => new NotifyNodeExecutor(_httpClientFactory),
150154
"TRANSFORM" => new TransformNodeExecutor(),
155+
"AI" => new AiNodeExecutor(
156+
_aiChatFunc ?? throw new InvalidOperationException("流程引擎未接入大模型,无法执行 AI 节点")),
151157
_ => throw new InvalidOperationException($"未知节点类型: {currentNode.ElementType}")
152158
};
153159

‎Juggle.Domain/Engine/FlowModels.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public class FlowNode
3535
[JsonPropertyName("mysqlConfig")]
3636
public MysqlConfig? MysqlConfig { get; set; }
3737

38+
[JsonPropertyName("aiConfig")]
39+
public AiConfig? AiConfig { get; set; }
40+
3841
[JsonPropertyName("subFlowConfig")]
3942
public SubFlowConfig? SubFlowConfig { get; set; }
4043

@@ -246,6 +249,22 @@ public class SubFlowMapping
246249
public string Target { get; set; } = "";
247250
}
248251

252+
/// <summary>AI 大模型节点配置(AI 节点)</summary>
253+
public class AiConfig
254+
{
255+
/// <summary>输入变量名(作为用户消息发送)</summary>
256+
[JsonPropertyName("input")]
257+
public string Input { get; set; } = "";
258+
259+
/// <summary>系统提示词(人设与任务说明)</summary>
260+
[JsonPropertyName("systemPrompt")]
261+
public string SystemPrompt { get; set; } = "";
262+
263+
/// <summary>输出变量名(模型回复写入)</summary>
264+
[JsonPropertyName("output")]
265+
public string Output { get; set; } = "";
266+
}
267+
249268
/// <summary>MySQL 节点配置(MYSQL 节点)</summary>
250269
public class MysqlConfig
251270
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Juggle.Domain.Engine.NodeExecutors;
2+
3+
/// <summary>
4+
/// AI 大模型节点执行器:把输入变量内容作为用户消息、按系统提示词调用大模型,
5+
/// 回复写入输出变量。对话函数由应用层注入(供应商/模型配置见系统设置 → 大模型设置)。
6+
/// </summary>
7+
public class AiNodeExecutor : INodeExecutor
8+
{
9+
private readonly Func<string, string, Task<string>> _chat;
10+
11+
public AiNodeExecutor(Func<string, string, Task<string>> chat) => _chat = chat;
12+
13+
public async Task<string?> ExecuteAsync(FlowNode node, FlowContext context)
14+
{
15+
var cfg = node.AiConfig
16+
?? throw new InvalidOperationException($"AI 节点 [{node.Key}] 未配置 aiConfig。");
17+
18+
var input = string.IsNullOrWhiteSpace(cfg.Input) ? "" : context.GetVariable(cfg.Input)?.ToString() ?? "";
19+
var systemPrompt = string.IsNullOrWhiteSpace(cfg.SystemPrompt) ? "你是一个智能助手。" : cfg.SystemPrompt;
20+
21+
var reply = await _chat(systemPrompt, input);
22+
23+
if (!string.IsNullOrWhiteSpace(cfg.Output))
24+
context.SetVariable(cfg.Output, reply);
25+
26+
return node.Outgoings.FirstOrDefault();
27+
}
28+
}

‎JuggleNet6.Frontend/src/views/flow/FlowDesign.vue‎

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,29 @@
279279
</div>
280280
</template>
281281

282+
<!-- AI 大模型节点属性 -->
283+
<template v-if="selectedNode.elementType === 'AI'">
284+
<div style="display:flex;justify-content:flex-end"><el-button size="small" icon="QuestionFilled" link @click="openNodeHelp('AI')">帮助</el-button></div>
285+
<div class="prop-tip">大模型节点:调用已配置的大模型(系统设置 → 大模型设置),把输入变量内容作为用户消息发送,回复写入输出变量。</div>
286+
<div class="prop-item">
287+
<label>输入变量</label>
288+
<el-select v-model="selectedNode.aiConfig.input" size="small" style="width:100%" clearable placeholder="选择输入变量(作为用户消息)">
289+
<el-option v-for="v in allVariables" :key="v.variableCode" :value="v.variableCode" :label="v.variableCode" />
290+
</el-select>
291+
</div>
292+
<div class="prop-item">
293+
<label>系统提示词</label>
294+
<el-input v-model="selectedNode.aiConfig.systemPrompt" type="textarea" :rows="4"
295+
placeholder="如:你是一名专业的文案专家,根据用户输入生成简洁有力的文案。" />
296+
</div>
297+
<div class="prop-item">
298+
<label>输出变量</label>
299+
<el-select v-model="selectedNode.aiConfig.output" size="small" style="width:100%" clearable placeholder="选择输出变量(模型回复写入)">
300+
<el-option v-for="v in allVariables" :key="v.variableCode" :value="v.variableCode" :label="v.variableCode" />
301+
</el-select>
302+
</div>
303+
</template>
304+
282305
<!-- NOTIFY 通知节点属性 -->
283306
<template v-if="selectedNode.elementType === 'NOTIFY'">
284307
<div style="display:flex;justify-content:flex-end"><el-button size="small" icon="QuestionFilled" link @click="openNodeHelp('NOTIFY')">帮助</el-button></div>
@@ -1802,6 +1825,31 @@ env_delay_ms = env_retry_count * 1000 + 500` },
18021825
场景: 多数据源并发查询 + 汇总。` }
18031826
]
18041827
},
1828+
AI: {
1829+
title: '大模型节点',
1830+
sections: [
1831+
{ title: '一、节点说明', code: `大模型节点调用系统设置 → 大模型设置中启用的供应商(OpenAI 兼容接口,支持 DeepSeek/通义千问/Kimi 等),适合在流程中插入 AI 能力:
1832+
- 内容生成 / 润色 / 翻译
1833+
- 文本分类 / 情感分析
1834+
- 结构化提取(要求模型返回 JSON 后用赋值节点解析)` },
1835+
{ title: '二、配置说明', code: `输入变量: 作为用户消息发送给模型(如 env_text)
1836+
系统提示词: 人设与任务说明(如"你是文案专家,输出简洁有力的文案")
1837+
输出变量: 模型回复写入的变量(如 env_ai_reply)` },
1838+
{ title: '三、使用 demo', code: `// 场景:流程中生成商品推荐文案
1839+
前置 ASSIGN 节点: env_product_name = "智能手表"
1840+
1841+
AI 节点:
1842+
输入变量: env_product_name
1843+
系统提示词: 你是一名电商文案专家,根据商品名称生成一句吸引人的推荐语
1844+
输出变量: env_ad_copy
1845+
1846+
后续节点用 ${'${env_ad_copy}'} 引用生成结果(如 NOTIFY 通知 / METHOD 参数)` },
1847+
{ title: '四、注意事项', code: `1. 需先在系统设置 → 大模型设置中配置并启用供应商,否则流程执行时报错
1848+
2. 输入变量不存在时以空文本发送
1849+
3. 需要结构化结果时,在系统提示词中要求"只输出 JSON",再用赋值节点/代码节点解析
1850+
4. 模型调用有网络延迟,建议设置节点超时(默认不限)` }
1851+
]
1852+
},
18051853
TRANSFORM: {
18061854
title: '模板转换节点',
18071855
sections: [
@@ -2650,7 +2698,7 @@ function nodeIcon(type: string) {
26502698
const map: Record<string, string> = {
26512699
START: '▶', END: '⏹', METHOD: '⚙', CONDITION: '◆',
26522700
ASSIGN: '←', CODE: '{ }', MYSQL: '⊕', MERGE: '⇒', SUB_FLOW: '⬡',
2653-
LOOP: '↻', DELAY: '⏱', PARALLEL: '∥', NOTIFY: '✉', TRANSFORM: '📝'
2701+
LOOP: '↻', DELAY: '⏱', PARALLEL: '∥', NOTIFY: '✉', TRANSFORM: '📝', AI: '🤖'
26542702
}
26552703
return map[type] || '?'
26562704
}
@@ -2659,7 +2707,7 @@ function nodeTypeName(type: string) {
26592707
const map: Record<string, string> = {
26602708
START: '开始', END: '结束', METHOD: '方法', CONDITION: '条件',
26612709
ASSIGN: '赋值', CODE: '代码', MYSQL: '数据库', MERGE: '聚合', SUB_FLOW: '子流程',
2662-
LOOP: '循环', DELAY: '延迟', PARALLEL: '并行', NOTIFY: '通知', TRANSFORM: '模板转换'
2710+
LOOP: '循环', DELAY: '延迟', PARALLEL: '并行', NOTIFY: '通知', TRANSFORM: '模板转换', AI: '大模型'
26632711
}
26642712
return map[type] || type
26652713
}
@@ -2680,6 +2728,7 @@ const nodeToolList = [
26802728
{ type: 'PARALLEL', icon: '∥', label: '并行' },
26812729
{ type: 'NOTIFY', icon: '✉', label: '通知' },
26822730
{ type: 'TRANSFORM', icon: '📝', label: '模板转换' },
2731+
{ type: 'AI', icon: '🤖', label: '大模型' },
26832732
]
26842733
26852734
function addNode(type: string) {
@@ -2729,6 +2778,9 @@ function addNode(type: string) {
27292778
if (type === 'TRANSFORM') bNode.transformConfig = {
27302779
targetType: 'VARIABLE', targetCode: '', template: ''
27312780
}
2781+
if (type === 'AI') bNode.aiConfig = {
2782+
input: '', systemPrompt: '', output: ''
2783+
}
27322784
27332785
businessNodes.value.push(bNode)
27342786
vfNodes.value.push(buildVfNode(bNode, x, y))

0 commit comments

Comments
 (0)