jiaxing.liao недель назад: 3
Родитель
Сommit
16590b9288

+ 4 - 4
.vscode/settings.json

@@ -10,11 +10,11 @@
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "i18n-ally.localesPaths": [
-    "src/renderer/src/locales"
+    "apps/web/src/i18n/locales"
   ],
-  "i18n-ally.enabledParsers": ["json"],
-  "i18n-ally.displayLanguage": "zh_CN",
-  "i18n-ally.sourceLanguage": "zh",
+  "i18n-ally.enabledParsers": ["ts", "json"],
+  "i18n-ally.displayLanguage": "zh-cn",
+  "i18n-ally.sourceLanguage": "zh-cn",
   "i18n-ally.keystyle": "nested",
   "i18n-ally.enabledFrameworks": ["vue"],
   "[vue]": {

+ 2 - 1
apps/web/.env

@@ -1 +1,2 @@
-VITE_BASE_URL=shalu-componenttesting-admin-dev.shalu.com
+# VITE_BASE_URL=shalu-componenttesting-admin-dev.shalu.com
+VITE_BASE_URL=shalu-componenttesting-c-dev.shalu.com

+ 1 - 0
apps/web/index.html

@@ -15,6 +15,7 @@
 
     <script src="/Views/SharedInclude/Permissions.js"></script>
     <script src="/Views/SharedInclude/BpmTools.js"></script>
+    <script src="/Content/Scripts/systemConfig.js"></script>
   </head>
   <body>
     <div id="app"></div>

+ 6 - 18
apps/web/src/features/RunWorkflow/components/TriggerTab.vue

@@ -21,23 +21,10 @@ const { t } = useI18n()
 const isScheduleTrigger = computed(() => props.nodeType === 'trigger-schedule')
 const isWebhookTrigger = computed(() => props.nodeType === 'trigger-webhook')
 const titleText = computed(() =>
-	props.isListening ? t('pages.runWorkflow.triggerPanel.listening') : t('pages.runWorkflow.triggerPanel.stopped')
+	props.isListening
+		? t('pages.runWorkflow.triggerPanel.listening')
+		: t('pages.runWorkflow.triggerPanel.stopped')
 )
-const descriptionText = computed(() => {
-	if (isScheduleTrigger.value) {
-		return props.isListening
-			? t('pages.runWorkflow.triggerPanel.scheduleListening')
-			: t('pages.runWorkflow.triggerPanel.scheduleStopped')
-	}
-
-	if (isWebhookTrigger.value) {
-		return props.isListening
-			? t('pages.runWorkflow.triggerPanel.webhookListening')
-			: t('pages.runWorkflow.triggerPanel.webhookStopped')
-	}
-
-	return ''
-})
 
 const iconName = computed(() =>
 	isScheduleTrigger.value ? 'lucide:calendar-clock' : 'lucide:webhook'
@@ -68,14 +55,15 @@ const copyWebhookUrl = async () => {
 			</div>
 
 			<div class="trigger-hero__title">{{ titleText }}</div>
-			<div class="trigger-hero__desc">{{ descriptionText }}</div>
 
 			<div v-if="isScheduleTrigger && nextScheduleRunText" class="trigger-hero__meta">
 				{{ t('pages.runWorkflow.triggerPanel.nextRun') }}{{ nextScheduleRunText }}
 			</div>
 
 			<div v-if="isWebhookTrigger && webhookDebugUrl" class="trigger-hero__url-row">
-				<span class="trigger-hero__url-label">{{ t('pages.runWorkflow.triggerPanel.webhookHint') }}</span>
+				<span class="trigger-hero__url-label">{{
+					t('pages.runWorkflow.triggerPanel.webhookHint')
+				}}</span>
 				<button type="button" class="trigger-hero__url" @click="copyWebhookUrl">
 					{{ webhookDebugUrl }}
 				</button>

+ 1 - 0
apps/web/src/features/RunWorkflow/index.vue

@@ -527,6 +527,7 @@ function statusText(status?: NodeStatus | RunnerStatus | null) {
 	if (status === 'failed') return t('pages.runWorkflow.failed')
 	if (status === 'error') return t('pages.runWorkflow.error')
 	if (status === 'connecting') return t('pages.runWorkflow.connecting')
+	if (status === 'suspended') return t('pages.runWorkflow.suspended')
 	return t('pages.runWorkflow.idle')
 }
 

+ 48 - 1
apps/web/src/i18n/locales/en-us.ts

@@ -1220,6 +1220,7 @@ export default {
 			success: 'Success',
 			failed: 'Failed',
 			error: 'Error',
+			suspended: 'Suspended',
 			drawerTitle: 'Run Workflow',
 			inputTab: 'Input',
 			triggerTab: 'Trigger',
@@ -1440,6 +1441,35 @@ export default {
 			keyPlaceholder: 'Enter a specified id',
 			pathRequired: 'Please enter the basic dataset path'
 		},
+		viewDataSetter: {
+			basicConfig: 'View Config',
+			code: 'View Name',
+			codePlaceholder: 'Enter the view name',
+			codeRequired: 'Please enter the view name',
+			resultType: 'Result Type',
+			resultTypeObject: 'Object',
+			resultTypeArray: 'Array',
+			resultTypeTree: 'Tree',
+			pageSize: 'Page Size',
+			treeConfig: 'Tree Config',
+			treeFieldId: 'ID Field',
+			treeFieldIdPlaceholder: 'Enter the mapped id field name',
+			treeFieldParentId: 'Parent ID Field',
+			treeFieldParentIdPlaceholder: 'Enter the mapped parentId field name',
+			treeFieldChildren: 'Children Field',
+			treeFieldChildrenPlaceholder: 'Enter the mapped children field name',
+			treeFieldText: 'Text Field',
+			treeFieldTextPlaceholder: 'Enter the mapped text field name',
+			treeFieldRequired: 'Please complete id, parentId and children fields in tree config',
+			indexVariable: 'Index Variable',
+			indexVariableDefaultDescribe: 'Index',
+			variableName: 'Name',
+			variableDescribe: 'Description',
+			variableType: 'Type',
+			variableValue: 'Value',
+			outputs: 'Outputs',
+			outputPreview: 'Output Structure Preview'
+		},
 		questionClassifierSetter: {
 			classes: 'Categories',
 			addClass: '+ Add Category',
@@ -1528,6 +1558,10 @@ export default {
 				displayName: 'Basic Dataset',
 				description: 'Read data from a configured basic dataset'
 			},
+			'view-data': {
+				displayName: 'View Data',
+				description: 'Read data from a configured view'
+			},
 			iteration: { displayName: 'Iteration', description: 'Iteration node' },
 			loop: { displayName: 'Loop', description: 'Loop node' },
 			'list-operator': { displayName: 'List Operations', description: 'List operation node' },
@@ -1546,7 +1580,15 @@ export default {
 			},
 			'loop-start': { displayName: 'Loop Start' },
 			'iteration-start': { displayName: 'Iteration Start' },
-			stickyNote: { displayName: 'Note', description: 'Markdown note block' }
+			stickyNote: { displayName: 'Note', description: 'Markdown note block' },
+			'mail-sender': {
+				displayName: 'Mail Sender',
+				description: 'Send emails to specified recipients'
+			},
+			'sms-sender': {
+				displayName: 'SMS Sender',
+				description: 'Send SMS messages to specified phone numbers'
+			}
 		},
 		outputs: {
 			http: {
@@ -1561,6 +1603,11 @@ export default {
 				rows: 'Query result rows',
 				rowCount: 'Query result row count'
 			},
+			'view-data': {
+				viewTable: 'View data result',
+				viewName: 'View name',
+				totalCount: 'Total count'
+			},
 			list: {
 				result: 'Filtered results',
 				firstRecord: 'First record',

+ 39 - 1
apps/web/src/i18n/locales/zh-cn.ts

@@ -1125,6 +1125,7 @@ export default {
 			success: '成功',
 			failed: '失败',
 			error: '异常',
+			suspended: '挂起',
 			drawerTitle: '运行工作流',
 			inputTab: '输入',
 			triggerTab: '触发',
@@ -1319,6 +1320,35 @@ export default {
 			keyPlaceholder: '请输入指定的 ID',
 			pathRequired: '请输入基础数据集路径'
 		},
+		viewDataSetter: {
+			basicConfig: '视图配置',
+			code: '视图名称',
+			codePlaceholder: '请输入视图名称',
+			codeRequired: '请输入视图名称',
+			resultType: '响应类型',
+			resultTypeObject: '对象',
+			resultTypeArray: '数组',
+			resultTypeTree: '树形',
+			pageSize: '分页大小',
+			treeConfig: '树配置',
+			treeFieldId: 'ID 字段',
+			treeFieldIdPlaceholder: '请输入映射后的 id 字段名',
+			treeFieldParentId: 'ParentId 字段',
+			treeFieldParentIdPlaceholder: '请输入映射后的 parentId 字段名',
+			treeFieldChildren: 'Children 字段',
+			treeFieldChildrenPlaceholder: '请输入映射后的 children 字段名',
+			treeFieldText: 'Text 字段',
+			treeFieldTextPlaceholder: '请输入映射后的 text 字段名',
+			treeFieldRequired: '树配置中的 id、parentId、children 字段必须填写',
+			indexVariable: '索引变量',
+			indexVariableDefaultDescribe: '索引',
+			variableName: '名称',
+			variableDescribe: '描述',
+			variableType: '类型',
+			variableValue: '值',
+			outputs: '输出变量',
+			outputPreview: '输出结构预览'
+		},
 		questionClassifierSetter: {
 			classes: '分类',
 			addClass: '+ 添加分类',
@@ -1409,6 +1439,7 @@ export default {
 		meta: {
 			'module-invoke': { displayName: '模块调用', description: '通过接口代码调用模块能力' },
 			'basic-dataset': { displayName: '基础数据集', description: '从配置好的基础数据集中读取数据' },
+			'view-data': { displayName: '视图数据', description: '从配置好的视图中读取数据' },
 			start: { displayName: '用户输入', description: '用户输入节点,用于接收用户输入' },
 			end: { displayName: '输出', description: '流程结束并输出节点' },
 			'http-request': { displayName: 'HTTP 请求', description: '通过 HTTP 请求获取数据' },
@@ -1427,7 +1458,9 @@ export default {
 			},
 			'loop-start': { displayName: '循环开始' },
 			'iteration-start': { displayName: '迭代开始' },
-			stickyNote: { displayName: '注释', description: 'Markdown 注释块' }
+			stickyNote: { displayName: '注释', description: 'Markdown 注释块' },
+			'mail-sender': { displayName: '邮件发送', description: '通过邮件发送信息' },
+			'sms-sender': { displayName: '短信发送', description: '通过短信发送信息' }
 		},
 		outputs: {
 			http: {
@@ -1442,6 +1475,11 @@ export default {
 				rows: '查询结果行',
 				rowCount: '查询结果总行数'
 			},
+			'view-data': {
+				viewTable: '视图数据结果',
+				viewName: '视图名称',
+				totalCount: '总数量'
+			},
 			list: {
 				result: '过滤结果',
 				firstRecord: '第一条记录',

+ 34 - 0
apps/web/src/nodes/Interface.ts

@@ -356,3 +356,37 @@ export interface ConditionType {
 		| 'array[object]'
 		| 'array[file]'
 }
+
+/**
+ * 文件类型
+ */
+export interface FileType {
+	id: string
+	name: string
+	extensionName: string
+	size: number
+	path: string
+}
+
+/**
+ * 文本类型
+ */
+export interface TextType {
+	id: string
+	parentId: string
+	isRoot: boolean
+	path: string
+	text: string
+	value: string
+	displayOrder: number
+}
+
+/**
+ * 输出视图数据
+ * view-data-output
+ */
+export interface ViewDataOutput {
+	viewTable: [] // "数据列表",
+	viewName: string // "视图名称",
+	totalCount: number // "数据总数",
+}

+ 1 - 1
apps/web/src/nodes/_base/CodeEditor.vue

@@ -100,7 +100,7 @@ const languageSource = [
 
 const getCompletionConfig = computed((): CodeEditorCompletionConfig => {
 	return {
-		java: javaCompletionConfig.value,
+		java: javaCompletionConfig.value?.java || [],
 		...props.completionConfig
 	}
 })

+ 1 - 0
apps/web/src/nodes/i18n.ts

@@ -10,6 +10,7 @@ const NODE_GROUP_KEYS: Record<string, 'start' | 'logic' | 'data' | 'other'> = {
 	code: 'logic',
 	'module-invoke': 'data',
 	'basic-dataset': 'data',
+	'view-data': 'data',
 	iteration: 'logic',
 	loop: 'logic',
 	'list-operator': 'data',

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

@@ -13,6 +13,10 @@ import { triggerScheduleNode } from './schedule'
 import { webhookNode } from './webhook'
 import { moduleInvokeNode } from './module-invoke'
 import { basicDatasetNode } from './basic-dataset'
+import { viewDataNode } from './view-data'
+import { smsSenderNode } from './sms-sender'
+import { mailSenderNode } from './mail-sender'
+
 import { getNodeDisplayName } from '@/nodes/i18n'
 import type { INodeType } from '../Interface'
 
@@ -69,7 +73,10 @@ const nodes = [
 	triggerScheduleNode,
 	webhookNode,
 	moduleInvokeNode,
-	basicDatasetNode
+	basicDatasetNode,
+	viewDataNode,
+	smsSenderNode,
+	mailSenderNode
 ]
 
 const nodeMap = nodes.reduce(

+ 51 - 0
apps/web/src/nodes/src/mail-sender/index.ts

@@ -0,0 +1,51 @@
+import { NodeConnectionTypes, type INodeDataBaseSchema, type INodeType } from '../../Interface'
+import Setter from './setter.vue'
+import i18n from '@/i18n'
+import { getNodeDescription, getNodeDisplayName } from '@/nodes/i18n'
+
+export type MailSenderData = INodeDataBaseSchema & {
+	subject: string // '主题'
+	body: string // '内容'
+	toLists: string // '收件人,多个以,分割'
+	ccLists: string // '抄送人,多个以,分割'
+	bccLists: string // '密抄人,多个以,分割'
+	attachmentLists: string // '附件id, 多个以,分割'
+	memo: string // '描述'
+	in_queue: boolean // '是否入队列'
+	delaySeconds: number // '入队列后延时几秒'
+}
+
+export const mailSenderNode: INodeType = {
+	version: ['1'],
+	displayName: getNodeDisplayName('mail-sender'),
+	name: 'mail-sender',
+	Setter,
+	description: getNodeDescription('mail-sender'),
+	group: 'other',
+	icon: 'lucide:mail',
+	iconColor: '#00c375',
+	inputs: [NodeConnectionTypes.main],
+	outputs: [NodeConnectionTypes.main],
+	// validate: (data: ModuleInvokeData) => {
+	// 	return data?.interface_code?.trim()
+	// 		? false
+	// 		: i18n.t('pages.moduleInvokeSetter.interfaceCodeRequired')
+	// },
+	// getSubtitle: (data: ModuleInvokeData) => {
+	// 	return data?.interface_code ? `code: ${data.interface_code}` : ''
+	// },
+	schema: {
+		appAgentId: '',
+		parentId: '',
+		position: {
+			x: 20,
+			y: 30
+		},
+		width: 96,
+		height: 96,
+		selected: false,
+		nodeType: 'mail-sender',
+		zIndex: 1,
+		data: {}
+	}
+}

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

@@ -0,0 +1,127 @@
+<template>
+	<el-scrollbar class="w-full box-border p-12px">
+		<div class="module-invoke-setter">
+			<InputVariables v-model="formData.variables" />
+			<section class="section-block">
+				<div class="section-title">主题</div>
+				<VarInput
+					v-model="formData.subject"
+					class="w-full"
+					placeholder="请输入主题;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">内容</div>
+				<VarInput
+					v-model="formData.body"
+					class="w-full"
+					:rows="3"
+					placeholder="请输入内容;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">收件人</div>
+				<VarInput
+					v-model="formData.toLists"
+					class="w-full"
+					placeholder="请输入收件人,多个以,分割;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">抄送人</div>
+				<VarInput
+					v-model="formData.ccLists"
+					class="w-full"
+					placeholder="请输入抄送人,多个以,分割;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">密抄人</div>
+				<VarInput
+					v-model="formData.bccLists"
+					class="w-full"
+					placeholder="请输入密抄人,多个以,分割;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">附件ID</div>
+				<VarInput
+					v-model="formData.attachmentLists"
+					class="w-full"
+					placeholder="请输入附件ID,多个以,分割;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">描述</div>
+				<VarInput
+					v-model="formData.memo"
+					class="w-full"
+					:rows="3"
+					placeholder="请输入描述;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="w-full flex items-center justify-between">
+					<div class="section-title">是否入队列</div>
+					<el-switch v-model="formData.in_queue"></el-switch>
+				</div>
+			</section>
+
+			<section v-if="formData.in_queue" class="section-block flex items-center justify-between">
+				<div class="section-title">延迟时间</div>
+				<el-input-number
+					v-model="formData.delaySeconds"
+					class="w-full"
+					placeholder="入队列后延时几秒"
+				/>
+			</section>
+		</div>
+	</el-scrollbar>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from '@/composables/useI18n'
+import { useSetterModel } from '../_shared/useSetterModel'
+
+import type { MailSenderData } from './index'
+import VarInput from '@/nodes/_base/VarInput.vue'
+import InputVariables from '@/nodes/_base/InputVariables.vue'
+
+interface Emits {
+	(e: 'update', value: MailSenderData): void
+}
+
+const props = defineProps<{
+	data: MailSenderData
+}>()
+
+const emit = defineEmits<Emits>()
+const { t } = useI18n()
+const formData = useSetterModel<MailSenderData>(props, emit)
+</script>
+
+<style scoped lang="less">
+.module-invoke-setter {
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+}
+
+.section-block {
+	padding-bottom: 16px;
+}
+
+.section-title {
+	margin-bottom: 12px;
+	font-size: 14px;
+	font-weight: 700;
+	color: #374151;
+}
+</style>

+ 3 - 4
apps/web/src/nodes/src/module-invoke/index.ts

@@ -23,10 +23,9 @@ export const moduleInvokeNode: INodeType = {
 			? false
 			: i18n.t('pages.moduleInvokeSetter.interfaceCodeRequired')
 	},
-	// getSubtitle: (node: ModuleInvokeData | { data?: ModuleInvokeData }) => {
-	// 	const interfaceCode = node?.interface_code ?? node?.data?.interface_code ?? ''
-	// 	return interfaceCode.split('\n').find((line) => line.trim())?.trim() || ''
-	// },
+	getSubtitle: (data: ModuleInvokeData) => {
+		return data?.interface_code ? `code: ${data.interface_code}` : ''
+	},
 	schema: {
 		appAgentId: '',
 		parentId: '',

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

@@ -7,7 +7,6 @@
 				<VarInput
 					v-model="formData.interface_code"
 					class="w-full"
-					:rows="6"
 					placeholder="请输入接口定义代码,支持输入/选择变量"
 				/>
 			</section>

+ 69 - 67
apps/web/src/nodes/src/question-classifier/setter.vue

@@ -1,6 +1,14 @@
 <template>
 	<el-scrollbar class="w-full box-border p-12px">
 		<div class="qc-setter">
+			<section class="section-block">
+				<div class="w-full flex items-center justify-between beautify">
+					<label class="text-14px font-bold text-gray-700">{{ texts.input }}</label>
+				</div>
+				<VarInput v-model="formData.query" class="w-full" placeholder="输入/选择变量" />
+			</section>
+
+			<!-- 分类 -->
 			<section class="section-block">
 				<div class="section-header">
 					<label class="section-title">{{ texts.classes }}</label>
@@ -9,67 +17,54 @@
 					</el-button>
 				</div>
 
-				<div class="w-full flex items-center justify-between beautify">
-					<label class="text-14px font-bold text-gray-700">{{ texts.input }}</label>
+				<div v-if="!formData.classes?.length" class="empty-state">
+					<div class="empty-desc">{{ texts.empty }}</div>
 				</div>
-				<VarInput v-model="formData.query" class="w-full" :rows="3" placeholder="输入/选择变量" />
-			</section>
 
-			<section class="section-block">
-				<el-collapse>
-					<el-collapse-item name="0">
-						<template #title>
-							<div class="flex items-center justify-between beautify">
-								<label class="text-14px font-bold text-gray-700">输出变量</label>
+					<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">
+								<Icon icon="lucide:grip-vertical" :size="16" class="handle drag-icon" />
+								<span class="class-index">{{ item.name }}</span>
 							</div>
-						</template>
-						<div>
-							<div v-if="!formData.classes?.length" class="empty-state">
-								<div class="empty-desc">{{ texts.empty }}</div>
+							<div class="class-card__actions">
+								<IconButton
+									link
+									icon="lucide:copy"
+									class="text-#667085"
+									@click="handleDuplicateClass(index)"
+								/>
+								<IconButton
+									link
+									icon="lucide:trash-2"
+									class="text-#f04438 ml-0!"
+									@click="handleRemoveClass(index)"
+								/>
 							</div>
+						</div>
 
-							<VueDraggable
-								v-else
-								v-model="formData.classes"
-								:animation="150"
-								handle=".handle"
-								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">
-											<Icon icon="lucide:grip-vertical" :size="16" class="handle drag-icon" />
-											<span class="class-index">{{ item.name }}</span>
-										</div>
-										<div class="class-card__actions">
-											<IconButton
-												link
-												icon="lucide:copy"
-												class="text-#667085"
-												@click="handleDuplicateClass(index)"
-											/>
-											<IconButton
-												link
-												icon="lucide:trash-2"
-												class="text-#f04438 ml-0!"
-												@click="handleRemoveClass(index)"
-											/>
-										</div>
-									</div>
-
-									<div class="class-card__body">
-										<VarInput
-											v-model="item.instruction"
-											class="w-full"
-											:rows="3"
-											:placeholder="texts.classInstructionPlaceholder"
-										/>
-									</div>
-								</div>
-							</VueDraggable>
+						<div class="class-card__body">
+							<VarInput
+								v-model="item.instruction"
+								class="w-full"
+								:rows="3"
+								:placeholder="texts.classInstructionPlaceholder"
+							/>
 						</div>
-					</el-collapse-item>
+					</div>
+				</VueDraggable>
+			</section>
 
+			<section class="section-block">
+				<el-collapse>
 					<el-collapse-item name="1">
 						<template #title>
 							<div class="flex items-center justify-between beautify">
@@ -112,7 +107,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, watch } from 'vue'
+import { computed } from 'vue'
 import { VueDraggable } from 'vue-draggable-plus'
 import { Icon, IconButton } from '@repo/ui'
 
@@ -149,33 +144,33 @@ const texts = computed(() => ({
 
 const formData = useSetterModel<QuestionClassifierData>(props, emit)
 
-watch(
-	() => formData.value.classes,
-	() => {
-		formData.value.classes.forEach((c, index) => {
-			c.id = `${props.id}_${index + 1}`
-			c.name = c.name || `${texts.value.classPrefix}${index + 1}`
-		})
-	},
-	{ deep: true }
-)
-
 const getId = () => {
 	const ids = formData.value.classes.map((c) => c.id.split('_')[1]).map(Number)
 	const maxId = Math.max(...ids, 0)
 	return props.id + '_' + (maxId + 1)
 }
 
+const getClassName = (index: number) => `${texts.value.classPrefix}${index}`
+
 const createClassItem = (index?: number): ClassItem => ({
 	id: index ? `${props.id}_${index}` : getId(),
-	name: '',
+	name: index ? getClassName(index) : '',
 	instruction: ''
 })
 
+const reorderClasses = () => {
+	formData.value.classes = (formData.value.classes || []).map((item, index) => ({
+		...item,
+		id: `${props.id}_${index + 1}`,
+		name: getClassName(index + 1)
+	}))
+}
+
 const ensureDefaults = () => {
 	formData.value.type ||= 'question-classifier'
 	formData.value.classes ||= [createClassItem(1), createClassItem(2), createClassItem(3)]
 	formData.value.instruction ||= ''
+	reorderClasses()
 }
 
 ensureDefaults()
@@ -185,10 +180,12 @@ const handleAddClass = () => {
 		formData.value.classes = []
 	}
 	formData.value.classes.push(createClassItem())
+	reorderClasses()
 }
 
 const handleRemoveClass = (index: number) => {
 	formData.value.classes.splice(index, 1)
+	reorderClasses()
 }
 
 const handleDuplicateClass = (index: number) => {
@@ -199,6 +196,11 @@ const handleDuplicateClass = (index: number) => {
 		id: `class-${Date.now()}-${index}`
 	}
 	formData.value.classes.splice(index + 1, 0, copy)
+	reorderClasses()
+}
+
+const handleSortEnd = () => {
+	reorderClasses()
 }
 </script>
 

+ 46 - 0
apps/web/src/nodes/src/sms-sender/index.ts

@@ -0,0 +1,46 @@
+import { NodeConnectionTypes, type INodeDataBaseSchema, type INodeType } from '../../Interface'
+import Setter from './setter.vue'
+import i18n from '@/i18n'
+import { getNodeDescription, getNodeDisplayName } from '@/nodes/i18n'
+
+export type SMSSenderData = INodeDataBaseSchema & {
+	config_name: string // '配置名称'
+	sign_name: string // '签名'
+	template_code: string // '模版编号'
+	senders: string // '发送者,多个以,分割'
+}
+
+export const smsSenderNode: INodeType = {
+	version: ['1'],
+	displayName: getNodeDisplayName('sms-sender'),
+	name: 'sms-sender',
+	Setter,
+	description: getNodeDescription('sms-sender'),
+	group: 'other',
+	icon: 'lucide:message-square-more',
+	iconColor: '#00c375',
+	inputs: [NodeConnectionTypes.main],
+	outputs: [NodeConnectionTypes.main],
+	// validate: (data: ModuleInvokeData) => {
+	// 	return data?.interface_code?.trim()
+	// 		? false
+	// 		: i18n.t('pages.moduleInvokeSetter.interfaceCodeRequired')
+	// },
+	// getSubtitle: (data: ModuleInvokeData) => {
+	// 	return data?.interface_code ? `code: ${data.interface_code}` : ''
+	// },
+	schema: {
+		appAgentId: '',
+		parentId: '',
+		position: {
+			x: 20,
+			y: 30
+		},
+		width: 96,
+		height: 96,
+		selected: false,
+		nodeType: 'sms-sender',
+		zIndex: 1,
+		data: {}
+	}
+}

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

@@ -0,0 +1,77 @@
+<template>
+	<el-scrollbar class="w-full box-border p-12px">
+		<div class="module-invoke-setter">
+			<InputVariables v-model="formData.variables" />
+			<section class="section-block">
+				<div class="section-title">配置名称</div>
+				<VarInput v-model="formData.config_name" class="w-full" placeholder="请输入配置名称" />
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">签名</div>
+				<VarInput
+					v-model="formData.sign_name"
+					class="w-full"
+					placeholder="请输入签名;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">模板编号</div>
+				<VarInput
+					v-model="formData.template_code"
+					class="w-full"
+					placeholder="请输入模板编号;键入/选择变量"
+				/>
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">发送者</div>
+				<VarInput
+					v-model="formData.senders"
+					class="w-full"
+					:rows="3"
+					placeholder="请输入发送者,多个以,分割;键入/选择变量"
+				/>
+			</section>
+		</div>
+	</el-scrollbar>
+</template>
+
+<script setup lang="ts">
+import { useSetterModel } from '../_shared/useSetterModel'
+
+import type { SMSSenderData } from './index'
+import VarInput from '@/nodes/_base/VarInput.vue'
+import InputVariables from '@/nodes/_base/InputVariables.vue'
+
+interface Emits {
+	(e: 'update', value: SMSSenderData): void
+}
+
+const props = defineProps<{
+	data: SMSSenderData
+}>()
+
+const emit = defineEmits<Emits>()
+const formData = useSetterModel<SMSSenderData>(props, emit)
+</script>
+
+<style scoped lang="less">
+.module-invoke-setter {
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+}
+
+.section-block {
+	padding-bottom: 16px;
+}
+
+.section-title {
+	margin-bottom: 12px;
+	font-size: 14px;
+	font-weight: 700;
+	color: #374151;
+}
+</style>

+ 90 - 0
apps/web/src/nodes/src/view-data/index.ts

@@ -0,0 +1,90 @@
+import {
+	NodeConnectionTypes,
+	type INodeDataBaseSchema,
+	type INodeType,
+	type NodeVariable,
+	type NodeVariableType
+} from '../../Interface'
+import Setter from './setter.vue'
+import i18n from '@/i18n'
+import { getNodeDescription, getNodeDisplayName } from '@/nodes/i18n'
+
+export type ViewDataResultType = 'object' | 'array' | 'tree'
+
+export interface ViewDataTreeFields {
+	id: string
+	parentId: string
+	children: string
+	text: string
+}
+
+export interface ViewDataTreeConfig {
+	fields: ViewDataTreeFields
+}
+
+export type ViewDataData = INodeDataBaseSchema & {
+	code: string
+	result_type: ViewDataResultType
+	tree_config: ViewDataTreeConfig
+	index_variable: NodeVariable
+	page_size: number
+}
+
+export const getViewDataTableOutputType = (
+	resultType: ViewDataResultType
+): Extract<NodeVariableType, 'object' | 'array[object]'> => {
+	return resultType === 'object' ? 'object' : 'array[object]'
+}
+
+export const createDefaultViewDataTreeConfig = (): ViewDataTreeConfig => ({
+	fields: {
+		id: 'id',
+		parentId: 'parentId',
+		children: 'children',
+		text: 'name'
+	}
+})
+
+export const viewDataNode: INodeType = {
+	version: ['1'],
+	displayName: getNodeDisplayName('view-data'),
+	name: 'view-data',
+	Setter,
+	description: getNodeDescription('view-data'),
+	group: 'data',
+	icon: 'lucide:table-properties',
+	iconColor: '#0ba5ec',
+	inputs: [NodeConnectionTypes.main],
+	outputs: [NodeConnectionTypes.main],
+	validate: (data: ViewDataData) => {
+		if (!data?.code?.trim()) {
+			return i18n.t('pages.viewDataSetter.codeRequired')
+		}
+
+		if (data?.result_type === 'tree') {
+			const fields = data?.tree_config?.fields
+			if (!fields?.id?.trim()) return i18n.t('pages.viewDataSetter.treeFieldRequired')
+			if (!fields?.parentId?.trim()) return i18n.t('pages.viewDataSetter.treeFieldRequired')
+			if (!fields?.children?.trim()) return i18n.t('pages.viewDataSetter.treeFieldRequired')
+		}
+
+		return false
+	},
+	getSubtitle: (data: ViewDataData) => {
+		return data?.code || ''
+	},
+	schema: {
+		appAgentId: '',
+		parentId: '',
+		position: {
+			x: 20,
+			y: 30
+		},
+		width: 96,
+		height: 96,
+		selected: false,
+		nodeType: 'view-data',
+		zIndex: 1,
+		data: {}
+	}
+}

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

@@ -0,0 +1,262 @@
+<template>
+	<el-scrollbar class="w-full box-border p-12px">
+		<div class="view-data-setter">
+			<section class="section-block">
+				<div class="section-title">{{ texts.basicConfig }}</div>
+
+				<el-form label-position="top">
+					<el-form-item :label="texts.code">
+						<VarInput v-model="formData.code" :placeholder="texts.codePlaceholder" class="w-full" />
+					</el-form-item>
+
+					<el-form-item :label="texts.resultType">
+						<el-select v-model="formData.result_type" class="w-full">
+							<el-option
+								v-for="option in resultTypeOptions"
+								:key="option.value"
+								:label="option.label"
+								:value="option.value"
+							/>
+						</el-select>
+					</el-form-item>
+
+					<el-form-item :label="texts.pageSize">
+						<el-input-number
+							v-model="formData.page_size"
+							:min="1"
+							:max="9999"
+							class="w-full"
+							style="width: 100%"
+						/>
+					</el-form-item>
+
+					<el-form-item label="分页">
+						<VarInput
+							v-model="formData.index_variable.value"
+							:placeholder="texts.variableValue"
+							class="w-full"
+						/>
+					</el-form-item>
+				</el-form>
+			</section>
+
+			<section v-if="formData.result_type === 'tree'" class="section-block">
+				<div class="section-title">{{ texts.treeConfig }}</div>
+
+				<el-form label-position="top">
+					<div class="grid-row">
+						<el-form-item :label="texts.treeFieldId" class="grid-col">
+							<VarInput
+								v-model="tree_config.fields.id"
+								:placeholder="texts.treeFieldIdPlaceholder"
+								class="w-full"
+							/>
+						</el-form-item>
+
+						<el-form-item :label="texts.treeFieldParentId" class="grid-col">
+							<VarInput
+								v-model="tree_config.fields.parentId"
+								:placeholder="texts.treeFieldParentIdPlaceholder"
+								class="w-full"
+							/>
+						</el-form-item>
+					</div>
+
+					<div class="grid-row">
+						<el-form-item :label="texts.treeFieldChildren" class="grid-col">
+							<VarInput
+								v-model="tree_config.fields.children"
+								:placeholder="texts.treeFieldChildrenPlaceholder"
+								class="w-full"
+							/>
+						</el-form-item>
+
+						<el-form-item :label="texts.treeFieldText" class="grid-col">
+							<VarInput
+								v-model="tree_config.fields.text"
+								:placeholder="texts.treeFieldTextPlaceholder"
+								class="w-full"
+							/>
+						</el-form-item>
+					</div>
+				</el-form>
+			</section>
+
+			<section class="section-block">
+				<InputVariables v-model="formData.variables" />
+			</section>
+
+			<section class="section-block">
+				<div class="section-title">{{ texts.outputs }}</div>
+
+				<ul>
+					<li v-for="output in formData.outputs" :key="output.name">
+						<div>
+							<span class="text-#333">{{ output.name }}</span>
+							<span class="text-#999 ml-8px">{{ output.type }}</span>
+						</div>
+						<div class="text-#666">{{ output.describe }}</div>
+					</li>
+				</ul>
+
+				<div class="output-preview">
+					<div class="output-preview__title">{{ texts.outputPreview }}</div>
+					<CodeEditor
+						:model-value="preCode"
+						language="json"
+						:tools="false"
+						:copy-code="false"
+						:allow-fullscreen="false"
+						:allow-change-language="false"
+						:read-only="true"
+						:height="220"
+					/>
+				</div>
+			</section>
+		</div>
+	</el-scrollbar>
+</template>
+
+<script setup lang="ts">
+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 { useI18n } from '@/composables/useI18n'
+import { useSetterModel } from '../_shared/useSetterModel'
+
+import type { ViewDataData, ViewDataResultType } from './index'
+import { createDefaultViewDataTreeConfig } from './index'
+
+interface Emits {
+	(e: 'update', value: ViewDataData): void
+}
+
+const props = defineProps<{
+	data: ViewDataData
+}>()
+
+const emit = defineEmits<Emits>()
+const { t } = useI18n()
+const formData = useSetterModel<ViewDataData>(props, emit)
+
+const texts = computed(() => ({
+	basicConfig: t('pages.viewDataSetter.basicConfig'),
+	code: t('pages.viewDataSetter.code'),
+	codePlaceholder: t('pages.viewDataSetter.codePlaceholder'),
+	resultType: t('pages.viewDataSetter.resultType'),
+	pageSize: t('pages.viewDataSetter.pageSize'),
+	treeConfig: t('pages.viewDataSetter.treeConfig'),
+	treeFieldId: t('pages.viewDataSetter.treeFieldId'),
+	treeFieldIdPlaceholder: t('pages.viewDataSetter.treeFieldIdPlaceholder'),
+	treeFieldParentId: t('pages.viewDataSetter.treeFieldParentId'),
+	treeFieldParentIdPlaceholder: t('pages.viewDataSetter.treeFieldParentIdPlaceholder'),
+	treeFieldChildren: t('pages.viewDataSetter.treeFieldChildren'),
+	treeFieldChildrenPlaceholder: t('pages.viewDataSetter.treeFieldChildrenPlaceholder'),
+	treeFieldText: t('pages.viewDataSetter.treeFieldText'),
+	treeFieldTextPlaceholder: t('pages.viewDataSetter.treeFieldTextPlaceholder'),
+	indexVariable: t('pages.viewDataSetter.indexVariable'),
+	indexVariableDefaultDescribe: t('pages.viewDataSetter.indexVariableDefaultDescribe'),
+	variableName: t('pages.viewDataSetter.variableName'),
+	variableDescribe: t('pages.viewDataSetter.variableDescribe'),
+	variableType: t('pages.viewDataSetter.variableType'),
+	variableValue: t('pages.viewDataSetter.variableValue'),
+	outputs: t('pages.viewDataSetter.outputs'),
+	outputPreview: t('pages.viewDataSetter.outputPreview'),
+	resultTypeObject: t('pages.viewDataSetter.resultTypeObject'),
+	resultTypeArray: t('pages.viewDataSetter.resultTypeArray'),
+	resultTypeTree: t('pages.viewDataSetter.resultTypeTree')
+}))
+
+const resultTypeOptions = computed<Array<{ label: string; value: ViewDataResultType }>>(() => [
+	{ label: texts.value.resultTypeObject, value: 'object' },
+	{ label: texts.value.resultTypeArray, value: 'array' },
+	{ label: texts.value.resultTypeTree, value: 'tree' }
+])
+
+const preCode = `{
+	// "数据列表"
+	"viewTable":   [],
+	"viewName":    "视图名称",
+	"totalCount":  0
+}`
+
+const tree_config = computed({
+	get() {
+		return formData.value.tree_config?.fields
+			? formData.value.tree_config
+			: createDefaultViewDataTreeConfig()
+	},
+	set(value) {
+		formData.value.tree_config = value
+	}
+})
+</script>
+
+<style scoped lang="less">
+.view-data-setter {
+	display: flex;
+	flex-direction: column;
+	gap: 16px;
+}
+
+.section-block {
+	padding-bottom: 16px;
+}
+
+.section-title {
+	margin-bottom: 12px;
+	font-size: 14px;
+	font-weight: 700;
+	color: #374151;
+}
+
+.grid-row {
+	display: grid;
+	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+	gap: 12px;
+}
+
+.grid-col {
+	min-width: 0;
+}
+
+.grid-col--sm {
+	max-width: 180px;
+}
+
+.index-variable-card {
+	padding: 14px;
+	border: 1px solid #eaecf0;
+	border-radius: 12px;
+	background: #fff;
+}
+
+.output-list {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+	margin: 0;
+	padding: 0;
+	list-style: none;
+}
+
+.output-item {
+	padding: 12px 14px;
+	border: 1px solid #eaecf0;
+	border-radius: 12px;
+	background: #fff;
+}
+
+.output-preview {
+	margin-top: 12px;
+}
+
+.output-preview__title {
+	margin-bottom: 8px;
+	font-size: 13px;
+	font-weight: 600;
+	color: #475467;
+}
+</style>

+ 39 - 2
apps/web/src/store/modules/runner.store.ts

@@ -2,9 +2,9 @@ import { defineStore } from 'pinia'
 import { computed, reactive, readonly, ref } from 'vue'
 import { dayjs } from 'element-plus'
 
-export type RunnerStatus = 'idle' | 'connecting' | 'running' | 'finished' | 'error'
+export type RunnerStatus = 'idle' | 'connecting' | 'running' | 'finished' | 'error' | 'suspended'
 
-export type NodeStatus = 'idle' | 'running' | 'success' | 'failed'
+export type NodeStatus = 'idle' | 'running' | 'success' | 'failed' | 'suspended'
 
 export interface RunnerNodeInfo {
 	nodeId: string
@@ -83,6 +83,15 @@ interface AgentFinishMessage extends AgentRunnerMessageBase {
 	result: any
 }
 
+interface AgentErrorMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_AGENT_ERROR_MSG'
+	errorMsg: string
+}
+
+interface AgentSuspendMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_AGENT_SUSPEND_MSG'
+}
+
 export type AgentRunnerMessage =
 	| ConnectErrorMessage
 	| WelcomeMessage
@@ -94,6 +103,8 @@ export type AgentRunnerMessage =
 	| NodeIterationStepMessage
 	| NodeIterationFinishMessage
 	| AgentFinishMessage
+	| AgentErrorMessage
+	| AgentSuspendMessage
 
 const AGENT_RUNNER_WS_BASE = `wss://${import.meta.env.VITE_BASE_URL}/api/ws/agentRunner`
 
@@ -313,6 +324,32 @@ export const useRunnerStore = defineStore('runner', () => {
 				closeSocket()
 				break
 			}
+
+			/**
+			 * 智能体运行异常失败
+			 */
+			case 'CMD_AGENT_ERROR_MSG': {
+				const msg = data as AgentErrorMessage
+				status.value = 'error'
+				agentResult.value = msg.errorMsg || '智能体运行异常失败'
+
+				const execution = getCurrentExecution()
+				if (execution) {
+					execution.status = 'finished'
+					execution.finishedAt = msg.time || new Date().toISOString()
+					execution.result = msg.errorMsg || '智能体运行异常失败'
+				}
+
+				closeSocket()
+				break
+			}
+
+			/**
+			 * 智能体运行被挂起
+			 */
+			case 'CMD_AGENT_SUSPEND_MSG': {
+				
+			}
 			default:
 				break
 		}

+ 1 - 0
apps/web/src/views/editor/NodeView.vue

@@ -52,6 +52,7 @@
 	<el-popover
 		v-if="libaryRefferenceRef"
 		:visible="showNodeLibary"
+		trigger="manual"
 		:show-arrow="false"
 		:append-to="workflowWrapperRef"
 		:virtual-ref="libaryRefferenceRef"

+ 15 - 0
apps/web/vite.config.ts

@@ -67,6 +67,21 @@ export default defineConfig(({ mode }) => {
 					target: `http://${env.VITE_BASE_URL}`,
 					changeOrigin: true,
 					rewrite: (path) => path.replace(/^\/api/, '/api')
+				},
+				'/Content': {
+					target: `http://${env.VITE_BASE_URL}`,
+					changeOrigin: true,
+					rewrite: (path) => path.replace(/^\/Content/, '/Content')
+				},
+				'/Views': {
+					target: `http://${env.VITE_BASE_URL}`,
+					changeOrigin: true,
+					rewrite: (path) => path.replace(/^\/Views/, '/Views')
+				},
+				'/File': {
+					target: `http://${env.VITE_BASE_URL}`,
+					changeOrigin: true,
+					rewrite: (path) => path.replace(/^\/File/, '/File')
 				}
 			}
 		}

+ 0 - 1
packages/workflow/src/components/Canvas.vue

@@ -451,7 +451,6 @@ defineExpose({
 		:nodes="getNodes"
 		:edges="getEdges"
 		:default-viewport="defaultViewport"
-		:fit-view-on-init="autoFit"
 		:is-valid-connection="isValidConnection"
 		:translate-extent="translateExtent"
 		:node-extent="nodeExtent"