瀏覽代碼

fix: 修改节点异常与重试配置

jiaxing.liao 3 周之前
父節點
當前提交
bb06e89beb

+ 9 - 9
apps/web/index.html

@@ -6,16 +6,16 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>Shalu Agent</title>
 
-    <script src="/Content/Lib/jquery-1.11.3/jquery-1.11.3.js"></script>
-    <script src="/Content/Lib/jquery-1.10.2/jquery.cookie.js"></script>
-    <script src="/Content/Lib/axios/1.5.1/axios.min.js"></script>
-    <script src="/Content/Lib/jquery-1.11.3/file-md5.js"></script>
-    <script src="/Content/Lib/jsencrypt/3.3.2/jsencrypt.min.js"></script>
-    <script src="/Content/Lib/crypto-js/4.2.0/crypto-js.min.js"></script>
+    <script defer src="/Content/Lib/jquery-1.11.3/jquery-1.11.3.js"></script>
+    <script defer src="/Content/Lib/jquery-1.10.2/jquery.cookie.js"></script>
+    <script defer src="/Content/Lib/axios/1.5.1/axios.min.js"></script>
+    <script defer src="/Content/Lib/jquery-1.11.3/file-md5.js"></script>
+    <script defer src="/Content/Lib/jsencrypt/3.3.2/jsencrypt.min.js"></script>
+    <script defer src="/Content/Lib/crypto-js/4.2.0/crypto-js.min.js"></script>
 
-    <script src="/Views/SharedInclude/Permissions.js"></script>
-    <script src="/Views/SharedInclude/BpmTools.js"></script>
-    <script src="/Content/Scripts/systemConfig.js"></script>
+    <script defer src="/Views/SharedInclude/Permissions.js"></script>
+    <script defer src="/Views/SharedInclude/BpmTools.js"></script>
+    <script defer src="/Content/Scripts/systemConfig.js"></script>
   </head>
   <body>
     <div id="app"></div>

+ 5 - 4
apps/web/src/components/Sidebar/index.vue

@@ -198,17 +198,18 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted, defineAsyncComponent } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
-import SearchDialog from '../SearchDialog/index.vue'
-import TemplateModal from '../TemplateModal/index.vue'
-import CreateWorkflowModal from '@/features/createModal/index.vue'
 import { useI18n } from '@/composables/useI18n'
 import { applyTheme } from '@/theme'
 
 import logo from '@/assets/logo.svg'
 
+const SearchDialog = defineAsyncComponent(() => import('../SearchDialog/index.vue'))
+const TemplateModal = defineAsyncComponent(() => import('../TemplateModal/index.vue'))
+const CreateWorkflowModal = defineAsyncComponent(() => import('@/features/createModal/index.vue'))
+
 const emit = defineEmits<{
 	'settings-menu-toggle': [position: { top: number; left: number }, name: string]
 }>()

+ 29 - 0
apps/web/src/nodes/_base/NodeRuntimeConfig.vue

@@ -0,0 +1,29 @@
+<template>
+	<section class="runtime-config">
+		<RetryConfig v-model="formData.retry_config" />
+		<ErrorHandling
+			v-model:error_strategy="formData.error_strategy"
+			v-model:default_value="formData.default_value"
+			:outputs="formData.outputs"
+		/>
+	</section>
+</template>
+
+<script setup lang="ts">
+import ErrorHandling from '@/nodes/_base/ErrorHandling.vue'
+import RetryConfig from '@/nodes/_base/RetryConfig.vue'
+
+const formData = defineModel({
+	type: Object,
+	default: () => ({})
+})
+</script>
+
+<style scoped lang="less">
+.runtime-config {
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+	padding: 0 12px 12px;
+}
+</style>

+ 47 - 3
apps/web/src/nodes/src/_shared/useSetterModel.ts

@@ -1,11 +1,54 @@
 import { ref, watch, type Ref } from 'vue'
 import { cloneDeep, isEqual } from 'lodash-es'
+import type { NodeErrorStrategy, NodeRetryConfig } from '@/nodes/Interface'
+
+const DEFAULT_RETRY_CONFIG: NodeRetryConfig = {
+	retry_enabled: false,
+	max_retries: 0,
+	retry_interval: 100
+}
+
+const VALID_ERROR_STRATEGY: NodeErrorStrategy[] = ['none', 'default-value', 'fail-branch']
+
+const ensureRuntimeDefaults = <T>(value: T): T => {
+	if (!value || typeof value !== 'object') {
+		return value
+	}
+
+	const data = value as Record<string, any>
+	const rawRetryConfig = data.retry_config || {}
+	data.retry_config = {
+		retry_enabled: Boolean(rawRetryConfig.retry_enabled),
+		max_retries:
+			typeof rawRetryConfig.max_retries === 'number'
+				? Math.max(0, Math.floor(rawRetryConfig.max_retries))
+				: DEFAULT_RETRY_CONFIG.max_retries,
+		retry_interval:
+			typeof rawRetryConfig.retry_interval === 'number'
+				? Math.max(100, Math.floor(rawRetryConfig.retry_interval))
+				: DEFAULT_RETRY_CONFIG.retry_interval
+	}
+
+	if (!VALID_ERROR_STRATEGY.includes(data.error_strategy)) {
+		data.error_strategy = 'none'
+	}
+
+	if (!Array.isArray(data.default_value)) {
+		data.default_value = []
+	}
+
+	if (typeof data.fail_branch_node_id !== 'string') {
+		data.fail_branch_node_id = ''
+	}
+
+	return value
+}
 
 export const useSetterModel = <T>(
 	props: { data: T },
 	emit: (event: 'update', value: T) => void
 ): Ref<T> => {
-	const formData = ref<T>(cloneDeep(props.data)) as Ref<T>
+	const formData = ref<T>(ensureRuntimeDefaults(cloneDeep(props.data))) as Ref<T>
 
 	watch(
 		() => props.data,
@@ -14,8 +57,9 @@ export const useSetterModel = <T>(
 				return
 			}
 
-			if (!isEqual(newVal, formData.value)) {
-				formData.value = cloneDeep(newVal)
+			const nextValue = ensureRuntimeDefaults(cloneDeep(newVal))
+			if (!isEqual(nextValue, formData.value)) {
+				formData.value = nextValue
 			}
 		},
 		{ deep: true, immediate: true }

+ 5 - 10
apps/web/src/nodes/src/basic-dataset/setter.vue

@@ -6,11 +6,7 @@
 
 				<el-form label-position="top">
 					<el-form-item :label="texts.path">
-						<VarInput
-							v-model="formData.path"
-							:placeholder="texts.pathPlaceholder"
-							class="w-full"
-						/>
+						<VarInput v-model="formData.path" :placeholder="texts.pathPlaceholder" class="w-full" />
 					</el-form-item>
 
 					<el-form-item :label="texts.group">
@@ -22,14 +18,12 @@
 					</el-form-item>
 
 					<el-form-item :label="texts.key">
-						<VarInput
-							v-model="formData.key"
-							:placeholder="texts.keyPlaceholder"
-							class="w-full"
-						/>
+						<VarInput v-model="formData.key" :placeholder="texts.keyPlaceholder" class="w-full" />
 					</el-form-item>
 				</el-form>
 			</section>
+
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -37,6 +31,7 @@
 <script setup lang="ts">
 import { computed, watch } from 'vue'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'

+ 2 - 0
apps/web/src/nodes/src/condition/setter.vue

@@ -28,6 +28,7 @@
 			<div class="text-16px font-bold">ELSE</div>
 			<div class="text-12px text-gray-500 pl-12px">用于定义当所有条件都不满足时的处理逻辑</div>
 		</div>
+		<NodeRuntimeConfig v-model="formData" />
 	</div>
 </template>
 
@@ -35,6 +36,7 @@
 import { computed } from 'vue'
 import Condition from '../../_base/condition/index.vue'
 import { Icon } from '@repo/ui'
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 
 import type { ConditionData } from './index'

+ 2 - 0
apps/web/src/nodes/src/end/setter.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import OutputVariables from '@/nodes/_base/OutputVariables.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 
@@ -18,5 +19,6 @@ const formData = useSetterModel<EndData>(props, emit)
 <template>
 	<el-scrollbar class="box-border p-12px">
 		<OutputVariables v-model="formData.outputs" :set-type="false" :set-value="true" />
+		<NodeRuntimeConfig v-model="formData" />
 	</el-scrollbar>
 </template>

+ 1 - 5
apps/web/src/nodes/src/http/index.ts

@@ -50,11 +50,7 @@ export const httpNode: INodeType = {
 	icon: 'lucide:link',
 	iconColor: '#875bf7',
 	inputs: [NodeConnectionTypes.main],
-	outputs: (data: HttpRequestData) => {
-		return data?.error_strategy === 'fail-branch'
-			? [NodeConnectionTypes.main, NodeConnectionTypes.main]
-			: [NodeConnectionTypes.main]
-	},
+	outputs: [NodeConnectionTypes.main],
 	validate: (data: HttpRequestData) => {
 		return !!data?.url ? false : i18n.t('pages.httpSetter.urlRequired')
 	},

+ 48 - 1
apps/web/src/nodes/src/index.ts

@@ -56,7 +56,52 @@ const iterationStartNode = {
 	}
 }
 
-const nodes = [
+/**
+ * 异常分支输出
+ * @param node
+ * @returns
+ */
+const withFailBranchOutput = (node: INodeType): INodeType => {
+	const baseOutputs = node.outputs
+
+	return {
+		...node,
+		outputs: (data: any) => {
+			const resolved = typeof baseOutputs === 'function' ? baseOutputs(data) : baseOutputs
+			const outputs = Array.isArray(resolved) ? [...resolved] : []
+
+			if (data?.error_strategy !== 'fail-branch') {
+				return outputs
+			}
+
+			const nodeId = typeof data?.id === 'string' ? data.id.trim() : ''
+			if (!nodeId) {
+				return outputs
+			}
+
+			const failOutputId = `${nodeId}_fail`
+			const hasFailOutput = outputs.some((item) => {
+				if (typeof item === 'string') {
+					return item === failOutputId
+				}
+
+				return !!item && typeof item === 'object' && 'id' in item && item.id === failOutputId
+			})
+
+			if (!hasFailOutput) {
+				outputs.push({
+					id: failOutputId,
+					type: 'port',
+					label: '异常'
+				})
+			}
+
+			return outputs
+		}
+	}
+}
+
+const baseNodes = [
 	startNode,
 	endNode,
 	httpNode,
@@ -79,6 +124,8 @@ const nodes = [
 	mailSenderNode
 ]
 
+const nodes = baseNodes.map((node) => withFailBranchOutput(node))
+
 const nodeMap = nodes.reduce(
 	(acc, cur) => {
 		acc[cur.name] = cur

+ 3 - 0
apps/web/src/nodes/src/iteration/setter.vue

@@ -1,6 +1,7 @@
 <script lang="ts" setup>
 import { computed, watch } from 'vue'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import VarSelect from '@/nodes/_base/VarSelect.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 import { useI18n } from '@/composables/useI18n'
@@ -149,6 +150,8 @@ watch(
 					<el-switch v-model="formData.flatten_output" />
 				</div>
 			</el-form-item>
+
+			<NodeRuntimeConfig v-model="formData" />
 		</el-form>
 	</el-scrollbar>
 </template>

+ 2 - 0
apps/web/src/nodes/src/list/setter.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import { computed, watch } from 'vue'
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
 import VarSelect from '@/nodes/_base/VarSelect.vue'
 import { VARIABLE_TYPE_OPERATORS } from '@/constant'
@@ -304,6 +305,7 @@ const handleLeftChange = (val: string) => {
 					</el-radio-group>
 				</div>
 			</el-form-item>
+			<NodeRuntimeConfig v-model="formData" />
 		</el-form>
 	</el-scrollbar>
 </template>

+ 2 - 0
apps/web/src/nodes/src/loop/setter.vue

@@ -2,6 +2,7 @@
 import { computed } from 'vue'
 import { IconButton } from '@repo/ui'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import Condition from '@/nodes/_base/condition/index.vue'
 import LoopVar from './components/LoopVar.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
@@ -92,6 +93,7 @@ const onUpdateOperator = (operator: 'and' | 'or') => {
 					</div>
 				</div>
 			</el-form-item>
+			<NodeRuntimeConfig v-model="formData" />
 		</el-form>
 	</el-scrollbar>
 </template>

+ 2 - 0
apps/web/src/nodes/src/mail-sender/setter.vue

@@ -82,11 +82,13 @@
 					placeholder="入队列后延时几秒"
 				/>
 			</section>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
 
 <script setup lang="ts">
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
 

+ 2 - 0
apps/web/src/nodes/src/module-invoke/setter.vue

@@ -10,6 +10,7 @@
 					placeholder="请输入接口定义代码,支持输入/选择变量"
 				/>
 			</section>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -17,6 +18,7 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
 

+ 10 - 8
apps/web/src/nodes/src/question-classifier/setter.vue

@@ -21,14 +21,14 @@
 					<div class="empty-desc">{{ texts.empty }}</div>
 				</div>
 
-					<VueDraggable
-						v-else
-						v-model="formData.classes"
-						:animation="150"
-						handle=".handle"
-						@end="handleSortEnd"
-						class="class-list"
-					>
+				<VueDraggable
+					v-else
+					v-model="formData.classes"
+					:animation="150"
+					handle=".handle"
+					@end="handleSortEnd"
+					class="class-list"
+				>
 					<div v-for="(item, index) in formData.classes" :key="item.id" class="class-card">
 						<div class="class-card__header">
 							<div class="class-card__title">
@@ -102,6 +102,7 @@
 					</el-collapse-item>
 				</el-collapse>
 			</section>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -111,6 +112,7 @@ import { computed } from 'vue'
 import { VueDraggable } from 'vue-draggable-plus'
 import { Icon, IconButton } from '@repo/ui'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'

+ 6 - 1
apps/web/src/nodes/src/schedule/setter.vue

@@ -114,6 +114,7 @@
 					</div>
 				</div>
 			</template>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -123,6 +124,7 @@ import { QuestionFilled } from '@element-plus/icons-vue'
 import { computed, watch } from 'vue'
 import { Icon } from '@repo/ui'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 import { useI18n } from '@/composables/useI18n'
 
@@ -302,7 +304,10 @@ const ensureScheduleState = () => {
 ensureScheduleState()
 
 watch(text, () => {
-	if (!formData.value.title || formData.value.title === t('nodes.meta.trigger-schedule.displayName')) {
+	if (
+		!formData.value.title ||
+		formData.value.title === t('nodes.meta.trigger-schedule.displayName')
+	) {
 		formData.value.title = text.value.title
 	}
 })

+ 2 - 0
apps/web/src/nodes/src/sms-sender/setter.vue

@@ -34,11 +34,13 @@
 					placeholder="请输入发送者,多个以,分割;键入/选择变量"
 				/>
 			</section>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
 
 <script setup lang="ts">
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 
 import type { SMSSenderData } from './index'

+ 15 - 3
apps/web/src/nodes/src/start/setter.vue

@@ -69,6 +69,7 @@
 					</div>
 				</div>
 			</VueDraggable>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 
@@ -108,11 +109,19 @@
 			</el-form-item>
 
 			<el-form-item :label="texts.variableName" prop="name">
-				<el-input v-model="dialogForm.name" :placeholder="texts.variableNamePlaceholder" clearable />
+				<el-input
+					v-model="dialogForm.name"
+					:placeholder="texts.variableNamePlaceholder"
+					clearable
+				/>
 			</el-form-item>
 
 			<el-form-item :label="texts.displayName" prop="label">
-				<el-input v-model="dialogForm.label" :placeholder="texts.displayNamePlaceholder" clearable />
+				<el-input
+					v-model="dialogForm.label"
+					:placeholder="texts.displayNamePlaceholder"
+					clearable
+				/>
 			</el-form-item>
 
 			<el-form-item
@@ -234,6 +243,7 @@ import { cloneDeep } from 'lodash-es'
 import { Icon, IconButton } from '@repo/ui'
 import { VueDraggable } from 'vue-draggable-plus'
 
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useSetterModel } from '../_shared/useSetterModel'
 import CodeEditor from '@/nodes/_base/CodeEditor.vue'
 import FileUploadInput from '@/features/fileUpload/FileUploadInput.vue'
@@ -808,7 +818,9 @@ const handleSaveVariable = async () => {
 		formData.value.variables[editingIndex.value] = nextVariable
 	}
 
-	ElMessage.success(editingIndex.value === -1 ? texts.value.variableAdded : texts.value.variableUpdated)
+	ElMessage.success(
+		editingIndex.value === -1 ? texts.value.variableAdded : texts.value.variableUpdated
+	)
 	handleCloseDialog()
 }
 </script>

+ 3 - 0
apps/web/src/nodes/src/view-data/setter.vue

@@ -113,6 +113,8 @@
 					/>
 				</div>
 			</section>
+
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -123,6 +125,7 @@ import { computed } from 'vue'
 import CodeEditor from '@/nodes/_base/CodeEditor.vue'
 import InputVariables from '@/nodes/_base/InputVariables.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
 

+ 7 - 1
apps/web/src/nodes/src/webhook/setter.vue

@@ -205,6 +205,7 @@
 					</div>
 				</div>
 			</section>
+			<NodeRuntimeConfig v-model="formData" />
 		</div>
 	</el-scrollbar>
 </template>
@@ -214,6 +215,7 @@ import { computed, ref, watch } from 'vue'
 import { cloneDeep, isEqual } from 'lodash-es'
 import { ElMessage } from 'element-plus'
 import { Icon, IconButton } from '@repo/ui'
+import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
 
 import CodeEditor from '@/nodes/_base/CodeEditor.vue'
 import { VARIABLE_TYPE_OPTIONS } from '@/constant'
@@ -420,7 +422,11 @@ watch(
 		} else if (formData.value.outputs[0]?.name === 'payload._webhook_raw') {
 			formData.value.outputs[0].describe = texts.value.rawRequestBody
 		}
-		if (!formData.value.title || formData.value.title === 'Webhook 触发' || formData.value.title === 'Webhook Trigger') {
+		if (
+			!formData.value.title ||
+			formData.value.title === 'Webhook 触发' ||
+			formData.value.title === 'Webhook Trigger'
+		) {
 			formData.value.title = texts.value.defaultTitle
 		}
 	},

+ 12 - 1
apps/web/vite.config.ts

@@ -17,7 +17,18 @@ export default defineConfig(({ mode }) => {
 	return {
 		base: './',
 		build: {
-			outDir: 'app-agent'
+			outDir: 'app-agent',
+			rollupOptions: {
+				output: {
+					manualChunks: {
+						framework: ['vue', 'vue-router', 'pinia'],
+						element: ['element-plus', '@element-plus/icons-vue'],
+						monaco: ['monaco-editor'],
+						lexical: ['lexical', 'lexical-vue'],
+						echarts: ['echarts']
+					}
+				}
+			}
 		},
 		plugins: [
 			vue(),