@@ -25,31 +25,36 @@
< / el-form-item >
< / el-form >
< el-empty v-if = "modelNode s.length === 0" description="该模板无需补全模型参数" :image-size="60" / >
< el-empty v-if = "flowWorkflowGroup s.length === 0" description="该模板无需补全模型参数" :image-size="60" / >
< div v-for = "node in modelNodes" :key="node.id" class="tc-node" >
< div class = "tc-node-title " >
< span class = "tc-node-nam e" > { { node . name || node . nodeCode } } < / span >
< el-tag v-if = "!node.modelConfig?.modelId" type="warning" size="small" > 未选择模型 < / el -tag >
< el-tag v-else-if = "node.modelConfig?.modelName" type="success" size="small" > {{ node.modelConfig.modelName }} < / el -tag >
< / div >
<!-- 主模板 / 子工作流 分组 : 递归展开整棵工作流树 , 每层模型节点单独补全 -- >
< div v-for = "group in flowWorkflowGroups" :key="group.wf.key" class="tc-group " >
< div v-if = "group.wf.key !== 'main'" class="tc-group-titl e" > {{ group.wf.pathLabel }} < / div >
< div class = "tc-node-model " >
< template v-if = "node.modelConfig?.modelId " >
< ! - - 系统模型 : 直接内联显示 API Key 输入框 , 保存时统一转换 - - >
< div v-if = "systemM odelN odeIds[node.id]" class="tc-api-key-inline" >
< el -input
v-model = "apiKeyInputs[node.id]"
type = "password"
show -password
placeholder = "该模型为系统内置模型,请输入你的 API Key"
size = "small"
clearable
/ >
< / div >
< el-button type = "primary" plain size = "small" @click ="openModelSelector(node)" > 重新选择模型 < / el -button >
< / template >
< el-button v-else type = "primary" plain size = "small" @click ="openModelSelector(node)" > 选择模型 < / el -button >
< div v-for = "m in group.models" :key="m.nodeKey" class=" tc-node" >
< div class = "tc- node-title " >
< span class = "tc-node-name" > { { m . node . name || m . node . nodeCode } } < / span >
< el-tag v-if = "!m.n ode.m odelConfig?.modelId" type="warning" size="small" > 未选择模型 < / el -tag >
< el-tag v-else-if = "m.node.modelConfig?.modelName" type="success" size="small" > {{ m.node.modelConfig.modelName }} < / el -tag >
< / div >
< div class = "tc-node-model" >
< template v-if = "m.node.modelConfig?.modelId" >
< ! - - 系统模型 : 直接内联显示 API Key 输入框 , 保存时统一转换 - - >
< div v-if = "systemModelNodeIds[m.nodeKey]" class="tc-api-key-inline" >
< el -input
v-model = "apiKeyInputs[m.nodeKey]"
type = "password"
show -password
placeholder = "该模型为系统内置模型,请输入你的 API Key"
size = "small"
clearable
/ >
< / div >
< el-button type = "primary" plain size = "small" @click ="openModelSelector(m)" > 重新选择模型 < / el -button >
< / template >
< el-button v-else type = "primary" plain size = "small" @click ="openModelSelector(m)" > 选择模型 < / el -button >
< / div >
< / div >
< / div >
< / template >
@@ -64,7 +69,7 @@
< ModelSelector
v-model = "modelSelectorVisible"
:default-model = "selectorDefaultModel"
:same-type-model-type = "selectorTargetN ode?.modelConfig?.modelType"
:same-type-model-type = "selectorTargetRef?.n ode?.modelConfig?.modelType"
@confirm ="handleModelConfirm"
/ >
@@ -77,7 +82,8 @@ import { ref, computed, watch } from 'vue';
import { ElMessage } from 'element-plus' ;
import ModelSelector from '/@/views/settings/workflow/component/ModelSelector.vue' ;
import { stripReadonlyFields , deepClone , buildModelRequestParamsPath , enrichResponseBodyMapping } from '/@/views/settings/workflow/component/modelParamUtils' ;
import { getWorkflowDetail , saveWorkflow } from '/@/api/settings/creation' ;
import { getWorkflowDetail } from '/@/api/settings/creation' ;
import { updateWorkflowFromTemplate , getSubFlowWorkflowDetail } from '/@/api/settings/workflow' ;
import { getModelManage , updateModelManage } from '/@/api/settings/modelConfigV2' ;
interface Props {
@@ -90,41 +96,129 @@ interface Emits {
( e : 'saved' , newId : string , flowName : string ) : void ;
}
// 工作流树节点:主模板与嵌套子工作流统一建模,递归展开 sub_flow 引用
interface FlowWorkflowNode {
key : string ; // 唯一 key:主模板 'main',子工作流 'sub:{workflowId}'
id : string ; // 工作流 id(保存时定位;主模板即来源模板 id)
flowName : string ; // 工作流原名(子工作流保存时沿用原名,不可改)
flowContent : any ; // 该工作流完整 flowContent(保存时随 subFlows 提交)
nodes : any [ ] ; // flowContent.nodes 引用
startNodeId : string ; // 开始节点 id( buildModelRequestParamsPath 契约需要)
pathLabel : string ; // 展示:'主模板' / '子工作流『xxx』'
hasModel : boolean ; // 该层是否存在 model 节点(决定子树是否放入 subFlows 提交)
children : FlowWorkflowNode [ ] ;
}
// 模型节点引用:携带全局唯一 nodeKey(避免不同工作流内节点 id 冲突)与所属工作流 key
interface ModelNodeRef {
wfKey : string ;
nodeKey : string ;
node : any ;
}
const props = defineProps < Props > ( ) ;
const emit = defineEmits < Emits > ( ) ;
const loading = ref ( false ) ;
const saving = ref ( false ) ;
const flowName = ref ( '' ) ;
const nodes = ref < any [ ] > ( [ ] ) ;
const templateFlowContent = ref < any > ( null ) ;
const flowTree = ref < FlowWorkflowNode | null > ( null ) ;
const modelRefs = ref < ModelNodeRef [ ] > ( [ ] ) ;
// 防环:已展开过的工作流 id(子工作流可能互相引用 / 引用主模板)
const visitedFlowIds = new Set < string > ( ) ;
const modelSelectorVisible = ref ( false ) ;
const selectorTargetNode = ref < any > ( null ) ;
const selectorTargetRef = ref < ModelNodeRef | null > ( null ) ;
const selectorDefaultModel = ref < any > ( null ) ;
// 补全 API Key:内联输入框暂存 Key,保存时统一转换为用户模型
const apiKeyInputs = ref < Record < string , string > > ( { } ) ;
const systemModelNodeIds = ref < Record < string , boolean > > ( { } ) ;
const modelNodes = computed ( ( ) => nodes . value . filter ( ( n ) => String ( n ? . nodeCode || '' ) . toLowerCase ( ) === 'model' ) ) ;
// 开始节点 id:模板 DSL 与工作流管理 DSL 结构一致(nodeCode === '__start__'),
// 供 buildModelRequestParamsPath 为「表单展示」字段补 valueSource 指向开始节点
const getStartNodeId = ( ) => {
const sn = nodes . value . find ( ( n ) => String ( n ? . nodeCode || '' ) . toLowerCase ( ) === '__start__' ) ;
return sn ? . id || '' ;
} ;
// 按工作流分组(主模板在前,子工作流按树序),供模板分组渲染
const flowWorkflowGroups = computed ( ( ) => {
const groups : { wf : FlowWorkflowNode ; models : ModelNodeRef [ ] } [ ] = [ ] ;
const walk = ( wf : FlowWorkflowNode ) => {
groups . push ( { wf , models : modelRefs . value . filter ( ( m ) => m . wfKey === wf . key ) } ) ;
for ( const c of wf . children ) walk ( c ) ;
} ;
if ( flowTree . value ) walk ( flowTree . value ) ;
return groups ;
} ) ;
const canSave = computed ( ( ) => {
if ( ! flowName . value . trim ( ) ) return false ;
// 所有模型节点都必须已绑定模型
if ( modelNodes . value . some ( ( n ) => ! n . modelConfig ? . modelId ) ) return false ;
// 整棵树 所有模型节点都必须已绑定模型
if ( modelRefs . value . some ( ( m ) => ! m . node . modelConfig ? . modelId ) ) return false ;
// 系统模型节点必须已填写 API Key(未补全不可保存)
if ( modelNodes . value . some ( ( n ) => systemModelNodeIds . value [ n . id ] && ! ( apiKeyInputs . value [ n . id ] || '' ) . trim ( ) ) ) return false ;
if ( modelRefs . value . some ( ( m ) => systemModelNodeIds . value [ m . nodeKey ] && ! ( apiKeyInputs . value [ m . nodeKey ] || '' ) . trim ( ) ) ) return false ;
return true ;
} ) ;
// 递归展开:遍历当前工作流 nodes 收集 model 节点;遇 sub_flow 递归拉取子工作流详情
const expandSubFlows = async ( wf : FlowWorkflowNode ) => {
for ( const n of wf . nodes ) {
const code = String ( n ? . nodeCode || '' ) . toLowerCase ( ) ;
if ( code === 'model' ) {
wf . hasModel = true ;
modelRefs . value . push ( { wfKey : wf . key , nodeKey : ` ${ wf . key } | ${ n . id } ` , node : n } ) ;
} else if ( code === 'sub_flow' ) {
const subWfId = String ( n ? . subConfig ? . workflowId || '' ) ;
if ( ! subWfId || visitedFlowIds . has ( subWfId ) ) continue ;
visitedFlowIds . add ( subWfId ) ;
try {
const detail = ( await getSubFlowWorkflowDetail ( subWfId ) ) ? . data ;
const fc = detail ? . flowContent || { } ;
const childNodes = Array . isArray ( fc . nodes ) ? fc . nodes : [ ] ;
const child : FlowWorkflowNode = {
key : 'sub:' + subWfId ,
id : subWfId ,
flowName : detail ? . flowName || '' ,
flowContent : fc ,
nodes : childNodes ,
startNodeId : childNodes . find ( ( x : any ) => String ( x ? . nodeCode || '' ) . toLowerCase ( ) === '__start__' ) ? . id || '' ,
pathLabel : '子工作流『' + ( detail ? . flowName || subWfId ) + '』' ,
hasModel : false ,
children : [ ] ,
} ;
wf . children . push ( child ) ;
await expandSubFlows ( child ) ;
} catch {
// 子工作流详情拉取失败:跳过该子流程,不阻塞主模板补全
}
}
}
} ;
// 构建主模板树节点:优先拉取模板完整 DSL(get 接口),失败则回退列表项 flowContent
const buildMainTree = async ( ) => {
let fc : any = null ;
let mainNodes : any [ ] = [ ] ;
let detailFlowName = '' ;
try {
const detail = ( await getWorkflowDetail ( props . template ? . id ) ) ? . data ;
fc = detail ? . flowContent || null ;
mainNodes = JSON . parse ( JSON . stringify ( fc ? . nodes || detail ? . nodeInputParams || [ ] ) ) ;
detailFlowName = detail ? . flowName || '' ;
} catch {
fc = props . template ? . raw ? . flowContent || props . template ? . flowContent ;
mainNodes = JSON . parse ( JSON . stringify ( fc ? . nodes || [ ] ) ) ;
}
flowTree . value = {
key : 'main' ,
id : String ( props . template ? . id || '' ) ,
flowName : detailFlowName || props . template ? . flowName || '' ,
flowContent : fc ,
nodes : mainNodes ,
startNodeId : mainNodes . find ( ( n : any ) => String ( n ? . nodeCode || '' ) . toLowerCase ( ) === '__start__' ) ? . id || '' ,
pathLabel : '主模板' ,
hasModel : false ,
children : [ ] ,
} ;
visitedFlowIds . add ( flowTree . value . id ) ;
if ( detailFlowName ) flowName . value = '我的-' + detailFlowName ;
} ;
watch (
( ) => props . modelValue ,
async ( val ) => {
@@ -132,21 +226,14 @@ watch(
loading . value = true ;
saving . value = false ;
flowName . value = '我的-' + ( props . template ? . flowName || '系统模板' ) ;
nodes . value = [ ] ;
templateFlowContent . value = null ;
flowTree . value = null ;
modelRefs . value = [ ] ;
visitedFlowIds . clear ( ) ;
apiKeyInputs . value = { } ;
systemModelNodeIds . value = { } ;
try {
// 优先拉取模板完整 DSL(get 接口),失败则回退列表项 flowContent
const res = await getWorkflowDetail ( props . template ? . id ) ;
const detail = res . data ;
templateFlowContent . value = detail ? . flowContent || null ;
nodes . value = JSON . parse ( JSON . stringify ( detail ? . flowContent ? . nodes || detail ? . nodeInputParams || [ ] ) ) ;
if ( detail ? . flowName ) flowName . value = '我的-' + detail . flowName ;
} catch {
const fc = props . template ? . raw ? . flowContent || props . template ? . flowContent ;
templateFlowContent . value = fc || null ;
nodes . value = JSON . parse ( JSON . stringify ( fc ? . nodes || [ ] ) ) ;
await buildMainTree ( ) ;
if ( flowTree . value ) await expandSubFlows ( flowTree . value ) ;
} finally {
// 先判断各已绑定模型节点是否需要补 API Key(系统模型),完成后再结束 loading,避免竞态
await checkModelApiKey ( ) ;
@@ -155,16 +242,31 @@ watch(
}
) ;
const openModelSelector = ( node : any ) => {
selectorTargetNode . value = node ;
const openModelSelector = ( m : ModelNodeRef ) => {
selectorTargetRef . value = m ;
selectorDefaultModel . value = null ;
modelSelectorVisible . value = true ;
} ;
// 取指定工作流(按 key)的开始节点 id,用于契约字段生成
const getWfStartNodeId = ( wfKey : string ) : string => {
const walk = ( wf : FlowWorkflowNode | null ) : string => {
if ( ! wf ) return '' ;
if ( wf . key === wfKey ) return wf . startNodeId ;
for ( const c of wf . children ) {
const r = walk ( c ) ;
if ( r ) return r ;
}
return '' ;
} ;
return walk ( flowTree . value ) ;
} ;
// 选模型:复刻工作流管理 handleModelConfirm 的初始化逻辑
const handleModelConfirm = ( model : any ) => {
const node = selectorTargetNode . value ;
if ( node ) {
const m = selectorTargetRef . value ;
if ( m ) {
const node = m . node ;
node . modelConfig = node . modelConfig || { } ;
node . modelConfig . modelId = model . id || '' ;
node . modelConfig . modelName = model . modelName ;
@@ -173,41 +275,38 @@ const handleModelConfirm = (model: any) => {
const params = stripReadonlyFields ( deepClone ( model . requestBodyMapping ? ? null ) ) ;
node . modelConfig . modelRequestParams = params ;
// 后端契约:基于编辑器参数树(含 __originIndex 原始索引)生成全部叶子字段扁平清单,与工作流管理保存一致
node . modelConfig . modelRequestParamsPath = buildModelRequestParamsPath ( params , getStartNodeId ( ) ) ;
node . modelConfig . modelRequestParamsPath = buildModelRequestParamsPath ( params , getWfStartNodeId ( m . wfKey ) ) ;
// 用模型 responseMapping 的中文描述填充 value,供工作流管理「引用上级节点输出」展示中文
node . modelConfig . modelResponseBodyMapping = enrichResponseBodyMapping ( model . responseBodyMapping , model . responseMapping ) ;
// 选中为用户模型,无需再补 API Key
systemModelNodeIds . value [ node . id ] = false ;
apiKeyInputs . value [ node . id ] = '' ;
systemModelNodeIds . value [ m . nodeKey ] = false ;
apiKeyInputs . value [ m . nodeKey ] = '' ;
}
modelSelectorVisible . value = false ;
selectorTargetNode . value = null ;
selectorTargetRef . value = null ;
} ;
// 打开弹窗后:判断每个已绑定模型节点是否为系统模型(需补 API Key)
const checkModelApiKey = async ( ) => {
const targets = modelNodes . value . filter ( ( n ) => n . modelConfig ? . modelId ) ;
const targets = modelRefs . value . filter ( ( m ) => m . node . modelConfig ? . modelId ) ;
await Promise . all (
targets . map ( async ( node ) => {
targets . map ( async ( m ) => {
try {
const res = await getModelManage ( node . modelConfig . modelId ) ;
const res = await getModelManage ( m . node . modelConfig . modelId ) ;
const model = res ? . data ? . modelManage || res ? . data ;
if ( model ? . systemModel === true ) {
systemModelNodeIds . value [ node . id ] = true ;
} else {
systemModelNodeIds . value [ node . id ] = false ;
apiKeyInputs . value [ node . id ] = '' ;
}
systemModelNodeIds . value [ m . nodeKey ] = model ? . systemModel === true ;
if ( ! systemModelNodeIds . value [ m . nodeKey ] ) apiKeyInputs . value [ m . nodeKey ] = '' ;
} catch {
// 详情获取失败:不强制补 Key,避免阻塞保存
systemModelNodeIds . value [ node . id ] = false ;
systemModelNodeIds . value [ m . nodeKey ] = false ;
}
} )
) ;
} ;
// 将系统模型克隆为用户模型并绑定到节点(保存时统一调用)
const convertSystemModel = async ( node : any ) => {
const convertSystemModel = async ( m : ModelNodeRef ) => {
const node = m . node ;
const res = await getModelManage ( node . modelConfig . modelId ) ;
const src = res ? . data ? . modelManage || res ? . data ;
if ( ! src ? . id ) throw new Error ( '模型详情获取失败' ) ;
@@ -219,7 +318,7 @@ const convertSystemModel = async (node: any) => {
modelSupplier : src . modelSupplier ,
baseUrl : src . baseUrl ,
responseType : src . responseType ? ? src . invokeType ,
apiKey : ( apiKeyInputs . value [ node . id ] || '' ) . trim ( ) ,
apiKey : ( apiKeyInputs . value [ m . nodeKey ] || '' ) . trim ( ) ,
enabled : src . enabled ,
chatModel : src . ChatModel ? ? src . chatModel ,
maxConcurrency : src . maxConcurrency ,
@@ -243,43 +342,66 @@ const convertSystemModel = async (node: any) => {
if ( ! newModelManage ? . id ) throw new Error ( '接口未返回新的用户模型' ) ;
// 更新节点绑定为新用户模型,不再需要补 Key
node . modelConfig . modelId = newModelManage . id ;
systemModelNodeIds . value [ node . id ] = false ;
apiKeyInputs . value [ node . id ] = '' ;
systemModelNodeIds . value [ m . nodeKey ] = false ;
apiKeyInputs . value [ m . nodeKey ] = '' ;
} ;
// 各工作流 DSL 缺 modelRequestParamsPath 时兜底生成(含嵌套子工作流)
const patchRequestParamsPath = ( wf : FlowWorkflowNode ) => {
for ( const n of wf . nodes ) {
if ( String ( n ? . nodeCode || '' ) . toLowerCase ( ) !== 'model' ) continue ;
const mc = n ? . modelConfig ;
if ( mc ? . modelRequestParams && ! mc . modelRequestParamsPath ) {
mc . modelRequestParamsPath = buildModelRequestParamsPath ( mc . modelRequestParams , wf . startNodeId ) ;
}
}
for ( const c of wf . children ) patchRequestParamsPath ( c ) ;
} ;
// 子树内是否含 model 节点(自身或其子孙):含模型的子树才提交更新,保证嵌套写回链路完整
const nodeHasModelInSubtree = ( wf : FlowWorkflowNode ) : boolean => wf . hasModel || wf . children . some ( nodeHasModelInSubtree ) ;
// 组装嵌套 subFlows:仅含模型的工作流子树才提交(无模型的子流程无需更新)
const buildSubFlows = ( wf : FlowWorkflowNode ) : any [ ] =>
wf . children
. filter ( ( c ) => nodeHasModelInSubtree ( c ) )
. map ( ( c ) => ( {
id : c . id ,
flowName : c . flowName ,
description : '' ,
flowContent : c . flowContent ,
subFlows : buildSubFlows ( c ) ,
} ) ) ;
const handleSave = async ( ) => {
const name = flowName . value . trim ( ) ;
if ( ! name ) {
ElMessage . warning ( '请输入工作流名称' ) ;
return ;
}
const missing = modelNodes . value . find ( ( n ) => ! n . modelConfig ? . modelId ) ;
const missing = modelRefs . value . find ( ( m ) => ! m . node . modelConfig ? . modelId ) ;
if ( missing ) {
ElMessage . warning ( ` 节点「 ${ missing . name || missing . nodeCode } 」未选择模型,请先选择 ` ) ;
ElMessage . warning ( ` 节点「 ${ missing . node . name || missing . node . nodeCode } 」未选择模型,请先选择 ` ) ;
return ;
}
saving . value = true ;
try {
// 保存前统一转换:系统模型节点先克隆为用户模型并绑定
for ( const node of modelNodes . value . filter ( ( n ) => systemModelNodeIds . value [ n . id ] ) ) {
await convertSystemModel ( node ) ;
}
// 模板 DSL 若缺 modelRequestParamsPath(旧版模板),用现有参数树兜底生成,保证后端拿到契约字段
const startNodeId = getStartNodeId ( ) ;
for ( const node of modelNodes . value ) {
const mc = node ? . modelConfig ;
if ( mc ? . modelRequestParams && ! mc . modelRequestParamsPath ) {
mc . modelRequestParamsPath = buildModelRequestParamsPath ( mc . modelRequestParams , startNodeId ) ;
}
// 保存前统一转换:整棵树的 系统模型节点先克隆为用户模型并绑定
for ( const m of modelRefs . value . filter ( ( mm ) => systemModelNodeIds . value [ mm . nodeKey ] ) ) {
await convertSystemModel ( m ) ;
}
// 旧模板缺 modelRequestParamsPath 的兜底生成(主模板 + 嵌套子工作流)
if ( flowTree . value ) patchRequestParamsPath ( flowTree . value ) ;
const flowContent = {
... ( templateFlowContent . value || { } ) ,
nodes : nodes . value ,
... ( flowTree . value ? . flowContent || { } ) ,
nodes : flowTree . value ? . nodes || [ ] ,
} ;
const res = await saveWorkflow ( {
const res = await updateWorkflowFromTemplate ( {
id : flowTree . value ? . id || String ( props . template ? . id || '' ) ,
flowName : name ,
description : '' ,
flowContent ,
subFlows : flowTree . value ? buildSubFlows ( flowTree . value ) : [ ] ,
} ) ;
const data = res ? . data ;
const newId = data ? . id ? ? ( typeof data === 'string' || typeof data === 'number' ? data : null ) ;
@@ -317,6 +439,21 @@ const handleClose = () => {
}
}
/* 主模板 / 子工作流 分组:子工作流组带标题条,与主模板节点区分 */
. tc - group {
margin - bottom : 16 px ;
}
. tc - group - title {
font - size : 12 px ;
font - weight : 600 ;
color : # 2563 eb ;
background : # eff6ff ;
border - radius : 6 px ;
padding : 6 px 10 px ;
margin - bottom : 10 px ;
}
. tc - node {
border : 1 px solid # e2e8f0 ;
border - radius : 10 px ;