diff --git a/CLAUDE.md b/CLAUDE.md index 551813f..e0c6d2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,7 @@ These rules capture long-term repository preferences confirmed by the user and s 5. **改动评分与总结**:每次修改完成后,对本次改动按 1-10 分打分,并附上总结和改进建议 6. **中文沟通**:所有与用户的沟通、说明、思考展示必须使用中文 7. **操作后清理**:每次文件操作(拆分、替换、脚本执行等)完成后,必须检查并清理产生的临时文件,不得遗留在项目目录中 +8. **内容创作与工作流管理必须隔离**:`src/views/settings/creation`(内容创作)是线上运行的旧版本,所有改动只服务于工作流管理(`src/views/settings/workflow`),绝对不修改内容创作的代码、API、组件或数据结构。工作流如需共享能力,应为自身新建独立版本(如独立的 `src/api/settings/workflow/`、本地组件),不得改动内容创作在用的东西。后端为工作流管理做的数据变化也属于工作流管理,不必适配内容创作。 ### Pagination Rules diff --git a/src/api/settings/workflow/index.ts b/src/api/settings/workflow/index.ts new file mode 100644 index 0000000..3718518 --- /dev/null +++ b/src/api/settings/workflow/index.ts @@ -0,0 +1,80 @@ +import request from '/@/utils/request'; + +// ===== 节点库(V2 结构,workflow 专用;creation 继续使用 creation/index.ts 的旧结构)===== +export interface NodeLibraryPresetOption { + value?: unknown; + field: string; + label: string; + type: string; + required: boolean; + options?: Array<{ + key: string; + value: string; + config?: NodeLibraryPresetOption[] | null; + }> | null; +} + +export interface NodeLibraryItem { + key: string; + name: string; + group: string; + sort: number; + desc?: string; + batchExecOption: boolean; + preToolOption: boolean; + postToolOption: boolean; + skillOption: boolean; + promptOption: boolean; + isSaveFileOption: boolean; + formConfigOption: boolean; + modelConfigOption: boolean; + presetOption: NodeLibraryPresetOption[] | null; +} + +export interface NodeLibraryGroup { + group: { key: string; name: string }; + nodes: NodeLibraryItem[]; +} + +export function getNodeLibraryList() { + return request({ + url: '/ai-agent/node/library/list', + method: 'get', + }) as Promise<{ code: number; message: string; data: { groups: NodeLibraryGroup[] } }>; +} + +// ===== 模型列表(不按类型过滤,显示全部)===== +export interface WorkflowModelItem { + id: string; + modelName: string; + modelType: number | string; + baseUrl?: string; + enabled?: number; + isOwner?: number; + [key: string]: any; +} + +export function getWorkflowModelList(params?: { pageNum: number; pageSize: number; modelName?: string }) { + return request({ + url: '/model-gateway/model/listModel', + method: 'get', + params, + }) as Promise<{ code: number; message: string; data: { list: WorkflowModelItem[]; total: number } }>; +} + +// ===== 技能列表 ===== +export interface WorkflowSkillItem { + id: number; + name: string; + description: string; + category: string; + [key: string]: any; +} + +export function getWorkflowSkillList(params?: { pageNum: number; pageSize: number; keyword?: string }) { + return request({ + url: '/ai-agent/skill/user/listUser', + method: 'get', + params, + }) as Promise<{ code: number; message: string; data: { list: WorkflowSkillItem[]; total: number } }>; +} diff --git a/src/views/settings/workflow/component/InputSourceManager.vue b/src/views/settings/workflow/component/InputSourceManager.vue deleted file mode 100644 index ccef596..0000000 --- a/src/views/settings/workflow/component/InputSourceManager.vue +++ /dev/null @@ -1,279 +0,0 @@ - - - 上级参数引用 - - - - - - {{ getNodeName(sourceNode.nodeId) }} - - - - {{ fieldName }} - 删除 - - - - emit('toggleOutput', sourceNode.nodeId, val)" - size="small" - active-text="引入输出" - inactive-text="" - /> - - - - - - - 上级节点输出 - - {{ parentNode.name }} - emit('toggleOutput', parentNode.id, val)" - size="small" - active-text="引入输出" - inactive-text="" - /> - - - - - - - - - - - - - - - diff --git a/src/views/settings/workflow/component/ModelSelector.vue b/src/views/settings/workflow/component/ModelSelector.vue new file mode 100644 index 0000000..c113024 --- /dev/null +++ b/src/views/settings/workflow/component/ModelSelector.vue @@ -0,0 +1,268 @@ + + + + + + + + 搜索 + + + + + + + + + {{ getModelTypeName(model.modelType) }} + + + + + + {{ model.modelName }} + {{ model.baseUrl }} + + + {{ model.enabled === 1 ? '已启用' : '已禁用' }} + + + + + + + + + + + + + 取消 + 确定 + + + + + + + diff --git a/src/views/settings/workflow/component/NodeConfigPanel.vue b/src/views/settings/workflow/component/NodeConfigPanel.vue index f527548..48fb489 100644 --- a/src/views/settings/workflow/component/NodeConfigPanel.vue +++ b/src/views/settings/workflow/component/NodeConfigPanel.vue @@ -11,7 +11,7 @@ - + 选择模型 @@ -31,7 +31,7 @@ - + - - - emit('removeField', nodeId, fieldName)" - @toggle-output="(nodeId: string, enabled: boolean) => emit('toggleOutput', nodeId, enabled)" - @add-param="(paramValue: string) => emit('addParamByValue', paramValue)" - /> @@ -123,52 +112,45 @@ + + diff --git a/src/views/settings/workflow/index.vue b/src/views/settings/workflow/index.vue index 27e5314..503e430 100644 --- a/src/views/settings/workflow/index.vue +++ b/src/views/settings/workflow/index.vue @@ -18,19 +18,12 @@ @@ -81,7 +74,7 @@ /> - + @@ -97,37 +90,30 @@ import { MiniMap } from '@vue-flow/minimap'; import { ElMessage, ElMessageBox } from 'element-plus'; import type { Node, Edge, Connection } from '@vue-flow/core'; import { - getNodeLibraryList, getWorkflowList, getWorkflowDetail, saveWorkflow, updateWorkflow, deleteWorkflow, - type NodeLibraryGroup, type WorkflowItem, } from '/@/api/settings/creation'; +import { getNodeLibraryList, type NodeLibraryGroup } from '/@/api/settings/workflow'; import NodeConfigPanel from './component/NodeConfigPanel.vue'; import NodeLibraryPanel from './component/NodeLibraryPanel.vue'; import WorkflowListPanel from './component/WorkflowListPanel.vue'; import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue'; -import ModelSelector from '/@/components/model/ModelSelector.vue'; -import SkillSelector from '/@/components/skill/SkillSelector.vue'; +import ModelSelector from './component/ModelSelector.vue'; +import SkillSelector from './component/SkillSelector.vue'; interface NodeData { label?: string; nodeCode?: string; - inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null; formConfig?: any[]; modelConfig?: any; skillName?: string; patchLayout?: boolean; } -interface ParamRef { - id: string; - label: string; -} - const { addNodes, addEdges, findNode, removeNodes, getNodes, updateNode } = useVueFlow(); // 常量定义 @@ -144,14 +130,26 @@ const filteredNodeLibraryGroups = computed(() => { // 节点配置映射:nodeCode -> 节点配置 const nodeConfigMap = computed(() => { - const map = new Map(); + const map = new Map< + string, + { + formConfig: any[]; + modelConfigOption: boolean; + formConfigOption: boolean; + skillOption: boolean; + promptOption: boolean; + isSaveFileOption: boolean; + } + >(); nodeLibraryGroups.value.forEach((group) => { - group.items.forEach((item) => { - map.set(item.nodeCode, { - formConfig: item.formConfig || [], - modelConfig: item.modelConfig || [], + group.nodes.forEach((item) => { + map.set(item.key, { + formConfig: item.presetOption || [], + modelConfigOption: item.modelConfigOption || false, + formConfigOption: item.formConfigOption || false, skillOption: item.skillOption || false, - patchLayout: item.patchLayout || false, + promptOption: item.promptOption || false, + isSaveFileOption: item.isSaveFileOption || false, }); }); }); @@ -186,14 +184,6 @@ let nodeId = 0; // 模型选择器相关状态 const showModelSelector = ref(false); const selectedModelData = ref(null); -const currentNodeModelType = computed(() => { - if (!selectedNode.value || !currentNodeConfig.value) return 0; - const modelConfig = currentNodeConfig.value.modelConfig; - if (modelConfig && modelConfig.length > 0) { - return modelConfig[0].modelType || 0; - } - return 0; -}); // 技能选择器相关状态 const showSkillSelector = ref(false); @@ -266,74 +256,6 @@ const updateSelectedNode = (updatedNode: Node) => { } }; -const availableParams = computed(() => { - if (!selectedNode.value) return []; - const parents: ParamRef[] = []; - const findParents = (nodeId: string, visited = new Set()) => { - if (visited.has(nodeId)) return; - visited.add(nodeId); - edges.value - .filter((e) => e.target === nodeId) - .forEach((edge) => { - const parent = findNode(edge.source); - if (parent?.data && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') { - parents.push({ id: parent.id, label: `${parent.data.label}.output` }); - } - findParents(edge.source, visited); - }); - }; - findParents(selectedNode.value.id); - return parents; -}); - -const addParam = (param: ParamRef) => { - if (!selectedNode.value?.data) return; - - const updatedNode: Node = { - ...selectedNode.value, - data: { - ...selectedNode.value.data, - inputSource: selectedNode.value.data.inputSource || [], - }, - }; - - // 查找是否已存在该节点的引用 - const existingIndex = updatedNode.data!.inputSource!.findIndex((item) => item.nodeId === param.id); - - if (existingIndex >= 0) { - // 已存在,添加 output 到 field 数组 - const existing = updatedNode.data!.inputSource![existingIndex]; - if (!existing.field.includes('output')) { - existing.field.push('output'); - selectedNode.value = updatedNode; - // 同步更新到 VueFlow 内部状态 - updateNode(updatedNode.id, updatedNode); - const index = nodes.value.findIndex((n) => n.id === updatedNode.id); - if (index >= 0) { - nodes.value[index] = updatedNode; - } - ElMessage.success('已添加参数引用'); - } else { - ElMessage.info('该参数已被引用'); - } - } else { - // 不存在,创建新的引用 - updatedNode.data!.inputSource!.push({ - nodeId: param.id, - field: ['output'], - quoteOutput: false, - }); - selectedNode.value = updatedNode; - // 同步更新到 VueFlow 内部状态 - updateNode(updatedNode.id, updatedNode); - const index = nodes.value.findIndex((n) => n.id === updatedNode.id); - if (index >= 0) { - nodes.value[index] = updatedNode; - } - ElMessage.success('已添加参数引用'); - } -}; - // 模型选择确认 const handleModelConfirm = (model: any) => { if (!selectedNode.value?.data) return; @@ -344,6 +266,7 @@ const handleModelConfirm = (model: any) => { ...selectedNode.value.data, modelConfig: { modelName: model.modelName, + modelType: model.modelType, modelApiKey: '', modelForm: model.modelForm || [], modelResponse: model.responseBody || {}, @@ -460,146 +383,6 @@ const handleTogglePatchLayout = (value: boolean) => { } }; -// 删除上级参数字段 -const handleRemoveField = (nodeId: string, fieldName: string) => { - if (!selectedNode.value?.data) return; - - const inputSource = selectedNode.value.data.inputSource || []; - const nodeIndex = inputSource.findIndex((item) => item.nodeId === nodeId); - if (nodeIndex < 0) return; - - const node = inputSource[nodeIndex]; - const newField = node.field.filter((f) => f !== fieldName); - - let updatedInputSource; - if (newField.length > 0) { - updatedInputSource = [...inputSource]; - updatedInputSource[nodeIndex] = { ...node, field: newField }; - } else { - updatedInputSource = inputSource.filter((_, idx) => idx !== nodeIndex); - } - - const updatedNode: Node = { - ...selectedNode.value, - data: { - ...selectedNode.value.data, - inputSource: updatedInputSource.length > 0 ? updatedInputSource : null, - }, - }; - - selectedNode.value = updatedNode; - // 同步更新到 VueFlow 内部状态 - updateNode(updatedNode.id, updatedNode); - const index = nodes.value.findIndex((n) => n.id === updatedNode.id); - if (index >= 0) { - nodes.value[index] = updatedNode; - } - - ElMessage.success(`已删除参数:${fieldName}`); -}; - -// 切换节点输出引用 -const handleToggleOutput = (nodeId: string, enabled: boolean) => { - if (!selectedNode.value?.data) return; - - const inputSource = selectedNode.value.data.inputSource || []; - const nodeIndex = inputSource.findIndex((item) => item.nodeId === nodeId); - - let updatedInputSource; - if (nodeIndex >= 0) { - updatedInputSource = [...inputSource]; - updatedInputSource[nodeIndex] = { - ...updatedInputSource[nodeIndex], - quoteOutput: enabled, - }; - } else { - updatedInputSource = [ - ...inputSource, - { - nodeId: nodeId, - field: [], - quoteOutput: enabled, - }, - ]; - } - - const updatedNode: Node = { - ...selectedNode.value, - data: { - ...selectedNode.value.data, - inputSource: updatedInputSource, - }, - }; - - selectedNode.value = updatedNode; - // 同步更新到 VueFlow 内部状态 - updateNode(updatedNode.id, updatedNode); - const index = nodes.value.findIndex((n) => n.id === updatedNode.id); - if (index >= 0) { - nodes.value[index] = updatedNode; - } - - const parentNode = nodes.value.find((n) => n.id === nodeId); - const nodeName = parentNode?.data?.label || '节点'; - ElMessage.success(enabled ? `已开启引入 ${nodeName} 的输出` : `已关闭引入 ${nodeName} 的输出`); -}; - -// 通过参数值添加上级参数 -const handleAddParamByValue = (paramValue: string) => { - if (!selectedNode.value?.data) return; - - const match = paramValue.match(/\$\{([^.]+)\.(.+)\}/); - if (!match) return; - - const nodeId = match[1]; - const paramName = match[2]; - - const inputSource = selectedNode.value.data.inputSource || []; - const existingIndex = inputSource.findIndex((item) => item.nodeId === nodeId); - - let updatedInputSource; - if (existingIndex >= 0) { - const existing = inputSource[existingIndex]; - if (!existing.field.includes(paramName)) { - updatedInputSource = [...inputSource]; - updatedInputSource[existingIndex] = { - ...existing, - field: [...existing.field, paramName], - }; - } else { - ElMessage.info('该参数已被引用'); - return; - } - } else { - updatedInputSource = [ - ...inputSource, - { - nodeId: nodeId, - field: [paramName], - quoteOutput: false, - }, - ]; - } - - const updatedNode: Node = { - ...selectedNode.value, - data: { - ...selectedNode.value.data, - inputSource: updatedInputSource, - }, - }; - - selectedNode.value = updatedNode; - // 同步更新到 VueFlow 内部状态 - updateNode(updatedNode.id, updatedNode); - const index = nodes.value.findIndex((n) => n.id === updatedNode.id); - if (index >= 0) { - nodes.value[index] = updatedNode; - } - - ElMessage.success(`已添加上级参数:${paramName}`); -}; - // 辅助函数:判断是否为开始节点 const isStartNode = (node: Node) => { return node.data?.nodeCode === START_NODE_CODE; @@ -686,7 +469,7 @@ const addNodeFromLibrary = (nodeCode: string, nodeName: string) => { id: `node-${++nodeId}`, type: 'default', position: { x: spawnX, y: spawnY }, - data: { label: nodeName, nodeCode, inputSource: null, patchLayout: false }, + data: { label: nodeName, nodeCode, patchLayout: false }, style: { background: '#fff', border: '2px solid #3b82f6', borderRadius: '8px', padding: '10px 20px' }, }, ]); @@ -714,7 +497,7 @@ const addDefaultStartNode = () => { id: 'start-node', type: 'input', position: { x: 200, y: 200 }, - data: { label: '开始', nodeCode: '__start__', inputSource: null }, + data: { label: '开始', nodeCode: '__start__' }, style: { background: '#10b981', color: '#fff', border: '2px solid #059669', borderRadius: '8px', padding: '10px 20px' }, }; addNodes([startNode]); @@ -757,23 +540,6 @@ const loadWorkflowFromDsl = (dsl: any) => { try { const loadedNodes = (dsl.nodes || []).map((n: any) => { - // 确保 inputSource 使用新结构 - let normalizedInputSource: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null = null; - - if (n.inputSource) { - if (Array.isArray(n.inputSource)) { - // 检查是否为新结构(对象数组) - if (n.inputSource.length > 0 && typeof n.inputSource[0] === 'object' && n.inputSource[0].nodeId) { - // 已经是新结构 - normalizedInputSource = n.inputSource; - } else { - // 旧结构(字符串数组),需要转换 - normalizedInputSource = []; - // 旧结构不再支持,设为空 - } - } - } - return { id: n.id, type: 'default', @@ -781,7 +547,6 @@ const loadWorkflowFromDsl = (dsl: any) => { data: { label: n.name || '', nodeCode: n.nodeCode, - inputSource: normalizedInputSource, formConfig: n.formConfig || null, modelConfig: n.modelConfig || null, skillName: n.skillName || null, @@ -855,7 +620,6 @@ const confirmSaveWorkflow = async () => { x: n.position?.x || 0, y: n.position?.y || 0, }, - inputSource: n.data?.inputSource || null, formConfig: n.data?.formConfig || null, modelConfig: n.data?.modelConfig || null, outputResult: null,
{{ model.baseUrl }}