瀏覽代碼

fix: 修改节点内容

jiaxing.liao 4 周之前
父節點
當前提交
0366581592

+ 44 - 0
apps/web/src/features/secretInput/SecretInput.vue

@@ -0,0 +1,44 @@
+<template>
+	<el-input
+		v-model="password"
+		type="password"
+		show-password
+		v-bind="$attrs"
+		@blur="encryptPassword"
+	/>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+const password = ref('')
+
+const modelValue = defineModel<{ rsa_aesKey: string; cipher_text: string }>()
+
+// 加密处理
+const encryptPassword = () => {
+	if (!password.value?.trim()) {
+		return
+	}
+
+	/// 获取AES秘钥
+	// @ts-ignore
+	const aesKey = window?.BpmTools?.$generateAESKey()
+
+	// 将AES秘钥加密
+	// @ts-ignore
+	const rsaAesKey = window?.BpmTools?.$encryptAESKey(aesKey)
+
+	// 进行AES算法加密密码
+	// @ts-ignore
+	const newPwd = window?.BpmTools?.$encryptDataWithAES(aesKey, password.value)
+
+	// rsaAesKey和newPwd构建db_password字段
+	modelValue.value = {
+		rsa_aesKey: rsaAesKey,
+		cipher_text: newPwd
+	}
+}
+</script>
+
+<style scoped></style>

+ 14 - 0
apps/web/src/i18n/locales/en-us.ts

@@ -1430,6 +1430,16 @@ export default {
 			interfaceCode: 'Interface Code',
 			interfaceCodeRequired: 'Please enter the interface code'
 		},
+		basicDatasetSetter: {
+			datasetConfig: 'Dataset Config',
+			path: 'Path',
+			pathPlaceholder: 'Enter the basic dataset path',
+			group: 'Group',
+			groupPlaceholder: 'Enter filter groups, separated by commas',
+			key: 'Key',
+			keyPlaceholder: 'Enter a specified id',
+			pathRequired: 'Please enter the basic dataset path'
+		},
 		questionClassifierSetter: {
 			classes: 'Categories',
 			addClass: '+ Add Category',
@@ -1514,6 +1524,10 @@ export default {
 				displayName: 'Module Invoke',
 				description: 'Invoke a module through interface code'
 			},
+			'basic-dataset': {
+				displayName: 'Basic Dataset',
+				description: 'Read data from a configured basic dataset'
+			},
 			iteration: { displayName: 'Iteration', description: 'Iteration node' },
 			loop: { displayName: 'Loop', description: 'Loop node' },
 			'list-operator': { displayName: 'List Operations', description: 'List operation node' },

+ 11 - 0
apps/web/src/i18n/locales/zh-cn.ts

@@ -1309,6 +1309,16 @@ export default {
 			interfaceCode: '接口代码',
 			interfaceCodeRequired: '请输入接口代码'
 		},
+		basicDatasetSetter: {
+			datasetConfig: '数据集配置',
+			path: '路径',
+			pathPlaceholder: '请输入基础数据集路径',
+			group: '过滤分组',
+			groupPlaceholder: '请输入过滤分组,多个以逗号分隔',
+			key: '指定 ID',
+			keyPlaceholder: '请输入指定的 ID',
+			pathRequired: '请输入基础数据集路径'
+		},
 		questionClassifierSetter: {
 			classes: '分类',
 			addClass: '+ 添加分类',
@@ -1398,6 +1408,7 @@ export default {
 		},
 		meta: {
 			'module-invoke': { displayName: '模块调用', description: '通过接口代码调用模块能力' },
+			'basic-dataset': { displayName: '基础数据集', description: '从配置好的基础数据集中读取数据' },
 			start: { displayName: '用户输入', description: '用户输入节点,用于接收用户输入' },
 			end: { displayName: '输出', description: '流程结束并输出节点' },
 			'http-request': { displayName: 'HTTP 请求', description: '通过 HTTP 请求获取数据' },

+ 26 - 4
apps/web/src/nodes/_base/CodeEditor.vue

@@ -79,6 +79,7 @@ const resizeState = reactive({
 	startHeight: props.height,
 	isDragging: false
 })
+const javaCompletionConfig = ref<CodeEditorCompletionConfig>()
 
 const isFullScreen = ref(false)
 const { monacoTheme, themeClass, toggleTheme } = useLocalEditorTheme({
@@ -97,6 +98,13 @@ const languageSource = [
 	{ id: 'java', name: 'java' }
 ]
 
+const getCompletionConfig = computed((): CodeEditorCompletionConfig => {
+	return {
+		java: javaCompletionConfig.value,
+		...props.completionConfig
+	}
+})
+
 /**
  * @description: formatValue为json 格式,需要转换处理
  * @return
@@ -110,9 +118,23 @@ const formatValue = (value: any) => {
 
 const syncCompletionConfig = () => {
 	if (!model) return
-	syncCodeEditorCompletionConfig(model, componentConfig.language, props.completionConfig)
+	syncCodeEditorCompletionConfig(model, componentConfig.language, getCompletionConfig.value)
+}
+
+const getJavaCompletionConfig = () => {
+	fetch('/Content/Lib/monacoEditor/completion/java.json')
+		.then((res) => res.json())
+		.then((data) => {
+			javaCompletionConfig.value = data?.java
+			syncCompletionConfig()
+		})
+		.catch(() => {
+			console.warn('Failed to load Java completion config.')
+		})
 }
 
+getJavaCompletionConfig()
+
 onMounted(() => {
 	// 处理代码转换
 	model = monaco.editor.createModel(formatValue(props.modelValue), componentConfig.language)
@@ -146,7 +168,7 @@ onMounted(() => {
 		if (!lastChange?.text) return
 
 		const triggerCharacters = new Set(['.'])
-		const languageRules = props.completionConfig?.[componentConfig.language] || []
+		const languageRules = getCompletionConfig.value?.[componentConfig.language] || []
 		languageRules.forEach((rule) => {
 			;(rule.triggerCharacters || []).forEach((char) => triggerCharacters.add(char))
 		})
@@ -163,7 +185,7 @@ onMounted(() => {
 				model,
 				position,
 				componentConfig.language,
-				props.completionConfig
+				getCompletionConfig.value
 			)
 		) {
 			monacoEditor.trigger('completion', 'editor.action.triggerSuggest', {})
@@ -201,7 +223,7 @@ watch(
 )
 
 watch(
-	() => props.completionConfig,
+	() => getCompletionConfig.value,
 	() => {
 		syncCompletionConfig()
 	},

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

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

+ 50 - 0
apps/web/src/nodes/src/basic-dataset/index.ts

@@ -0,0 +1,50 @@
+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 BasicDatasetData = INodeDataBaseSchema & {
+	path: string
+	group: string
+	key: string
+}
+
+export const basicDatasetNode: INodeType = {
+	version: ['1'],
+	displayName: getNodeDisplayName('basic-dataset'),
+	name: 'basic-dataset',
+	Setter,
+	description: getNodeDescription('basic-dataset'),
+	group: 'data',
+	icon: 'lucide:database',
+	iconColor: '#12b76a',
+	inputs: [NodeConnectionTypes.main],
+	outputs: [NodeConnectionTypes.main],
+	validate: (data: BasicDatasetData) => {
+		return data?.path?.trim() ? false : i18n.t('pages.basicDatasetSetter.pathRequired')
+	},
+	getSubtitle: (node: BasicDatasetData | { data?: BasicDatasetData }) => {
+		const data = node?.data || node
+		if (data?.key?.trim()) return `key: ${data.key}`
+		if (data?.group?.trim()) return `group: ${data.group}`
+		return data?.path || ''
+	},
+	schema: {
+		appAgentId: '',
+		parentId: '',
+		position: {
+			x: 20,
+			y: 30
+		},
+		width: 96,
+		height: 96,
+		selected: false,
+		nodeType: 'basic-dataset',
+		zIndex: 1,
+		data: {
+			path: '',
+			group: '',
+			key: ''
+		}
+	}
+}

+ 102 - 0
apps/web/src/nodes/src/basic-dataset/setter.vue

@@ -0,0 +1,102 @@
+<template>
+	<el-scrollbar class="w-full box-border p-12px">
+		<div class="basic-dataset-setter">
+			<section class="section-block">
+				<div class="section-title">{{ texts.datasetConfig }}</div>
+
+				<el-form label-position="top">
+					<el-form-item :label="texts.path">
+						<VarInput
+							v-model="formData.path"
+							:placeholder="texts.pathPlaceholder"
+							class="w-full"
+						/>
+					</el-form-item>
+
+					<el-form-item :label="texts.group">
+						<VarInput
+							v-model="formData.group"
+							:placeholder="texts.groupPlaceholder"
+							class="w-full"
+						/>
+					</el-form-item>
+
+					<el-form-item :label="texts.key">
+						<VarInput
+							v-model="formData.key"
+							:placeholder="texts.keyPlaceholder"
+							class="w-full"
+						/>
+					</el-form-item>
+				</el-form>
+			</section>
+		</div>
+	</el-scrollbar>
+</template>
+
+<script setup lang="ts">
+import { computed, watch } from 'vue'
+
+import VarInput from '@/nodes/_base/VarInput.vue'
+import { useI18n } from '@/composables/useI18n'
+import { useSetterModel } from '../_shared/useSetterModel'
+
+import type { BasicDatasetData } from './index'
+
+interface Emits {
+	(e: 'update', value: BasicDatasetData): void
+}
+
+const props = defineProps<{
+	data: BasicDatasetData
+}>()
+
+const emit = defineEmits<Emits>()
+const { t } = useI18n()
+const formData = useSetterModel<BasicDatasetData>(props, emit)
+
+const texts = computed(() => ({
+	datasetConfig: t('pages.basicDatasetSetter.datasetConfig'),
+	path: t('pages.basicDatasetSetter.path'),
+	pathPlaceholder: t('pages.basicDatasetSetter.pathPlaceholder'),
+	group: t('pages.basicDatasetSetter.group'),
+	groupPlaceholder: t('pages.basicDatasetSetter.groupPlaceholder'),
+	key: t('pages.basicDatasetSetter.key'),
+	keyPlaceholder: t('pages.basicDatasetSetter.keyPlaceholder')
+}))
+
+const ensureDefaults = () => {
+	formData.value.title ||= t('nodes.meta.basic-dataset.displayName')
+	formData.value.type ||= 'basic-dataset'
+	formData.value.path ||= ''
+	formData.value.group ||= ''
+	formData.value.key ||= ''
+}
+
+watch(
+	formData,
+	() => {
+		ensureDefaults()
+	},
+	{ deep: true, immediate: true }
+)
+</script>
+
+<style scoped lang="less">
+.basic-dataset-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>

+ 7 - 8
apps/web/src/nodes/src/database/index.ts

@@ -1,7 +1,7 @@
 import { NodeConnectionTypes, type INodeType, type INodeDataBaseSchema } from '../../Interface'
 import Setter from './setter.vue'
 import i18n from '@/i18n'
-import { getNodeDescription, getNodeDisplayName, getNodeOutputText } from '@/nodes/i18n'
+import { getNodeDescription, getNodeDisplayName } from '@/nodes/i18n'
 
 export type DatabaseType = 'mysql' | 'sqlserver' | 'oracle' | 'postgresql'
 
@@ -10,7 +10,12 @@ export type DatabaseData = INodeDataBaseSchema & {
 	db_host: string
 	db_port: number
 	db_username: string
-	db_encrypt_password: string
+	db_password: {
+		// aesKey的RSA加密后置
+		rsa_aesKey: string
+		// 密文
+		cipher_text: string
+	}
 	db_password_rsa_aesKey: string
 	db_name: string
 	db_properties: string
@@ -33,12 +38,6 @@ export const databaseNode: INodeType = {
 		if (!data?.db_host?.trim()) return i18n.t('pages.databaseSetter.dbHostRequired')
 		if (!data?.db_port) return i18n.t('pages.databaseSetter.dbPortRequired')
 		if (!data?.db_username?.trim()) return i18n.t('pages.databaseSetter.dbUsernameRequired')
-		if (!data?.db_encrypt_password?.trim()) {
-			return i18n.t('pages.databaseSetter.dbEncryptPasswordRequired')
-		}
-		if (!data?.db_password_rsa_aesKey?.trim()) {
-			return i18n.t('pages.databaseSetter.dbPasswordRsaAesKeyRequired')
-		}
 		if (!data?.db_name?.trim()) return i18n.t('pages.databaseSetter.dbNameRequired')
 		if (!data?.query_sql?.trim()) return i18n.t('pages.databaseSetter.querySqlRequired')
 		return false

+ 8 - 21
apps/web/src/nodes/src/database/setter.vue

@@ -58,20 +58,11 @@
 						</el-form-item>
 					</div>
 
-					<el-form-item :label="texts.dbEncryptPassword">
-						<VarInput
-							v-model="formData.db_encrypt_password"
-							:placeholder="texts.dbEncryptPasswordPlaceholder"
-							class="w-full var-input-textarea var-input-textarea--md"
-						/>
-					</el-form-item>
-
-					<el-form-item :label="texts.dbPasswordRsaAesKey">
-						<VarInput
-							v-model="formData.db_password_rsa_aesKey"
-							:placeholder="texts.dbPasswordRsaAesKeyPlaceholder"
-							class="w-full var-input-textarea var-input-textarea--md"
-						/>
+					<el-form-item
+						:label="texts.password"
+						tip="例如:alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt"
+					>
+						<SecretInput v-model="formData.db_password" class="w-full" />
 					</el-form-item>
 
 					<el-form-item :label="texts.dbProperties">
@@ -118,6 +109,7 @@ import { computed, watch } from 'vue'
 import ErrorHandling from '@/nodes/_base/ErrorHandling.vue'
 import RetryConfig from '@/nodes/_base/RetryConfig.vue'
 import VarInput from '@/nodes/_base/VarInput.vue'
+import SecretInput from '@/features/secretInput/SecretInput.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
 
@@ -146,10 +138,6 @@ const texts = computed(() => ({
 	dbPortPlaceholder: t('pages.databaseSetter.dbPortPlaceholder'),
 	dbUsername: t('pages.databaseSetter.dbUsername'),
 	dbUsernamePlaceholder: t('pages.databaseSetter.dbUsernamePlaceholder'),
-	dbEncryptPassword: t('pages.databaseSetter.dbEncryptPassword'),
-	dbEncryptPasswordPlaceholder: t('pages.databaseSetter.dbEncryptPasswordPlaceholder'),
-	dbPasswordRsaAesKey: t('pages.databaseSetter.dbPasswordRsaAesKey'),
-	dbPasswordRsaAesKeyPlaceholder: t('pages.databaseSetter.dbPasswordRsaAesKeyPlaceholder'),
 	dbName: t('pages.databaseSetter.dbName'),
 	dbNamePlaceholder: t('pages.databaseSetter.dbNamePlaceholder'),
 	dbProperties: t('pages.databaseSetter.dbProperties'),
@@ -159,7 +147,8 @@ const texts = computed(() => ({
 	mysql: t('pages.databaseSetter.types.mysql'),
 	sqlserver: t('pages.databaseSetter.types.sqlserver'),
 	oracle: t('pages.databaseSetter.types.oracle'),
-	postgresql: t('pages.databaseSetter.types.postgresql')
+	postgresql: t('pages.databaseSetter.types.postgresql'),
+	password: '密码'
 }))
 
 const dbTypeOptions = computed<Array<{ label: string; value: DatabaseType }>>(() => [
@@ -201,8 +190,6 @@ const ensureDefaults = () => {
 	formData.value.db_host ||= ''
 	formData.value.db_port ||= DEFAULT_PORT_MAP[formData.value.db_type]
 	formData.value.db_username ||= ''
-	formData.value.db_encrypt_password ||= ''
-	formData.value.db_password_rsa_aesKey ||= ''
 	formData.value.db_name ||= ''
 	formData.value.db_properties ||= ''
 	formData.value.query_sql ||= ''

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

@@ -12,6 +12,7 @@ import { loopEndNode } from './loop-end'
 import { triggerScheduleNode } from './schedule'
 import { webhookNode } from './webhook'
 import { moduleInvokeNode } from './module-invoke'
+import { basicDatasetNode } from './basic-dataset'
 import { getNodeDisplayName } from '@/nodes/i18n'
 import type { INodeType } from '../Interface'
 
@@ -67,7 +68,8 @@ const nodes = [
 	loopEndNode,
 	triggerScheduleNode,
 	webhookNode,
-	moduleInvokeNode
+	moduleInvokeNode,
+	basicDatasetNode
 ]
 
 const nodeMap = nodes.reduce(

+ 7 - 42
apps/web/src/nodes/src/module-invoke/setter.vue

@@ -1,13 +1,14 @@
 <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">{{ texts.interfaceCode }}</div>
-				<CodeEditor
+				<VarInput
 					v-model="formData.interface_code"
-					:completion-config="completionConfig"
-					language="javascript"
-					:height="240"
+					class="w-full"
+					:rows="6"
+					placeholder="请输入接口定义代码,支持输入/选择变量"
 				/>
 			</section>
 		</div>
@@ -17,53 +18,17 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 
-import CodeEditor from '@/nodes/_base/CodeEditor.vue'
 import { useI18n } from '@/composables/useI18n'
 import { useSetterModel } from '../_shared/useSetterModel'
 
 import type { ModuleInvokeData } from './index'
+import VarInput from '@/nodes/_base/VarInput.vue'
+import InputVariables from '@/nodes/_base/InputVariables.vue'
 
 interface Emits {
 	(e: 'update', value: ModuleInvokeData): void
 }
 
-const completionConfig = {
-	java: [
-		{
-			object: 'a',
-			methods: [
-				{
-					label: 'b',
-					insertText: 'b(${1:name})',
-					detail: 'String b(String name)',
-					documentation: '全局对象 a 的方法',
-					snippet: true
-				}
-			],
-			properties: [
-				{
-					label: 'version',
-					detail: 'String version'
-				}
-			]
-		}
-	],
-	javascript: [
-		{
-			object: 'Bpmtools',
-			methods: [
-				{
-					label: 'b',
-					insertText: 'b()',
-					detail: 'function b(): void',
-					documentation: '全局对象 a 的方法',
-					snippet: true
-				}
-			]
-		}
-	]
-}
-
 const props = defineProps<{
 	data: ModuleInvokeData
 }>()

+ 1 - 0
apps/web/src/nodes/src/question-classifier/index.ts

@@ -9,6 +9,7 @@ export type ClassItem = {
 }
 
 export type QuestionClassifierData = INodeDataBaseSchema & {
+	query: string
 	classes: ClassItem[]
 	instruction: string
 }

+ 99 - 56
apps/web/src/nodes/src/question-classifier/setter.vue

@@ -9,78 +9,110 @@
 					</el-button>
 				</div>
 
-				<div v-if="!formData.classes?.length" class="empty-state">
-					<div class="empty-desc">{{ texts.empty }}</div>
+				<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" :rows="3" placeholder="输入/选择变量" />
+			</section>
 
-				<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">{{ texts.classPrefix }} {{ index + 1 }}</span>
-								<el-input
-									v-model="item.name"
-									:placeholder="texts.classNamePlaceholder"
-									class="class-name-input"
-									clearable
-								/>
+			<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>
 							</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)"
-								/>
+						</template>
+						<div>
+							<div v-if="!formData.classes?.length" class="empty-state">
+								<div class="empty-desc">{{ texts.empty }}</div>
 							</div>
-						</div>
 
-						<div class="class-card__body">
+							<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>
+					</el-collapse-item>
+
+					<el-collapse-item name="1">
+						<template #title>
+							<div class="flex items-center justify-between beautify">
+								<label class="text-14px font-bold text-gray-700">{{
+									texts.advancedSettings
+								}}</label>
+							</div>
+						</template>
+						<div class="advanced-item">
+							<div class="advanced-label">{{ texts.instruction }}</div>
 							<VarInput
-								v-model="item.instruction"
+								v-model="formData.instruction"
 								class="w-full"
 								:rows="3"
-								:placeholder="texts.classInstructionPlaceholder"
+								placeholder="输入/选择变量"
 							/>
 						</div>
-					</div>
-				</VueDraggable>
-			</section>
+					</el-collapse-item>
 
-			<section class="section-block">
-				<div class="section-header">
-					<label class="section-title">{{ texts.advancedSettings }}</label>
-				</div>
-
-				<div class="advanced-item">
-					<div class="advanced-label">{{ texts.instruction }}</div>
-					<VarInput
-						v-model="formData.instruction"
-						class="w-full"
-						:rows="3"
-						:placeholder="texts.classInstructionPlaceholder"
-					/>
-				</div>
+					<el-collapse-item name="2">
+						<template #title>
+							<div class="flex items-center justify-between beautify">
+								<label class="text-14px font-bold text-gray-700">输出变量</label>
+							</div>
+						</template>
+						<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>
+					</el-collapse-item>
+				</el-collapse>
 			</section>
 		</div>
 	</el-scrollbar>
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, watch } from 'vue'
 import { VueDraggable } from 'vue-draggable-plus'
 import { Icon, IconButton } from '@repo/ui'
 
@@ -103,6 +135,7 @@ const emit = defineEmits<Emits>()
 const { t } = useI18n()
 
 const texts = computed(() => ({
+	input: t('pages.iterationSetter.input'),
 	classes: t('pages.questionClassifierSetter.classes'),
 	addClass: t('pages.questionClassifierSetter.addClass'),
 	empty: t('pages.questionClassifierSetter.empty'),
@@ -116,6 +149,17 @@ 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)
@@ -167,7 +211,6 @@ const handleDuplicateClass = (index: number) => {
 
 .section-block {
 	padding-bottom: 16px;
-	border-bottom: 1px solid #eeeeee;
 }
 
 .section-header {

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

@@ -30,7 +30,7 @@ export default defineConfig(({ mode }) => {
 			}),
 			// 代码编辑器
 			(monacoEditorPlugin as any).default({
-				languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html', 'css', 'java']
+				languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html', 'css']
 			}),
 			// 按需求加载(模板)
 			AutoImport({