Forráskód Böngészése

fix: 修改变量标签问题

jiaxing.liao 3 hete
szülő
commit
53650f77e8

+ 85 - 16
apps/web/src/components/VarLabel/index.vue

@@ -1,12 +1,18 @@
 <script setup lang="ts">
-import { computed } from 'vue'
+import type { NodeVar } from '@/types/var'
+import type { Ref } from 'vue'
+
+import { computed, inject } from 'vue'
 import { nodeMap } from '@/nodes'
+import { Icon } from '@repo/ui'
 
 const props = defineProps<{
 	label: string
-	nodeVars: any[]
+	nodeVars?: NodeVar[]
 }>()
 
+const injectedNodeVars = inject<Ref<NodeVar[]>>('nodeVars')
+
 const valueInfo = computed(() => {
 	// 根据#{xxx.var}解析变量
 	// 解析格式 #{env.xxx} 或 #{nodeId.xxx}
@@ -34,24 +40,28 @@ const valueInfo = computed(() => {
 		// 环境变量
 		return {
 			type: 'env',
-			name: varName,
+			name: 'env',
 			value: varName
 		}
 	} else if (prefix.startsWith('sys')) {
 		// 系统变量
 		return {
-			type: 'system',
-			name: props.label,
+			type: 'sys',
+			name: 'sys',
 			value: varName
 		}
 	} else {
 		// 节点变量,需要解析节点类型名称
-		if (props.nodeVars && Array.isArray(props.nodeVars)) {
-			const node = props.nodeVars.find((item) => item.type === 'output' && item.id === prefix)
+		const availableNodeVars = props.nodeVars ?? injectedNodeVars?.value
+
+		if (availableNodeVars?.length) {
+			const node = availableNodeVars.find((item) => item.id === prefix)
+
 			return {
 				type: 'node',
-				nodeType: node?.nodeType || '',
-				nodeName: node?.name || prefix,
+				icon: nodeMap?.[node?.type!]?.icon,
+				nodeType: node?.type || '',
+				nodeName: node?.name || '',
 				name: varName,
 				value: varName
 			}
@@ -59,7 +69,7 @@ const valueInfo = computed(() => {
 			return {
 				type: 'node',
 				nodeType: '',
-				nodeName: prefix,
+				nodeName: '',
 				name: varName,
 				value: varName
 			}
@@ -69,25 +79,84 @@ const valueInfo = computed(() => {
 </script>
 
 <template>
-	<div class="var-label">
+	<div class="var-label" :key="valueInfo.type + valueInfo.value">
 		<div v-if="valueInfo.type === 'env'" class="flex gap-1 items-center truncate">
 			<span class="var-select__item-prefix env text-6px">
 				<span>ENV</span>
 			</span>
 			<span class="text-gray-600">{{ valueInfo.value }}</span>
 		</div>
-		<div v-else class="truncate">
+
+		<div v-else-if="valueInfo.type === 'sys'" class="flex gap-1 items-center truncate">
+			<span class="var-select__item-prefix sys text-6px">
+				<span>SYS</span>
+			</span>
+			<span class="text-gray-600">{{ valueInfo.value }}</span>
+		</div>
+
+		<div
+			v-else
+			class="flex gap-1 items-center truncate"
+			:title="valueInfo.nodeName + ' / ' + valueInfo.value"
+		>
 			<span
 				class="var-select__item-prefix env text-10px"
 				:style="{ background: nodeMap[valueInfo?.nodeType ?? '']?.iconColor ?? '' }"
 			>
-				<Icon :icon="nodeMap[valueInfo?.nodeType!]?.icon!" :size="18" />
+				<Icon v-if="valueInfo?.icon" :icon="valueInfo?.icon!" :size="18" />
+				<span v-else class="text-6px">VAR</span>
 			</span>
-			<span>{{ valueInfo.nodeName }}</span>
-			<span class="mx-2px text-gray-400">/</span>
+			{{ valueInfo.nodeName }}
+			{{ valueInfo.nodeName ? '/' : '' }}
 			<span class="text-gray-600">{{ valueInfo.value }}</span>
+			<Icon icon="lucide:circle-alert" color="red" />
 		</div>
 	</div>
 </template>
 
-<style lang="less" scoped></style>
+<style lang="less" scoped>
+.var-label {
+	--el-tag-font-size: 12px;
+	--el-tag-border-radius: 4px;
+	--el-tag-bg-color: #f2f2f5;
+	background-color: var(--el-tag-bg-color);
+	vertical-align: middle;
+	height: 24px;
+	font-size: var(--el-tag-font-size);
+	border-radius: var(--el-tag-border-radius);
+	box-sizing: border-box;
+	white-space: nowrap;
+	border-width: 1px;
+	justify-content: center;
+	align-items: center;
+	padding: 0 4px;
+	line-height: 1;
+	display: inline-flex;
+}
+
+.var-select__item-prefix {
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	width: 20px;
+	height: 20px;
+	border-radius: 6px;
+	color: #fff;
+	flex-shrink: 0;
+
+	&.env {
+		background: #6366f1;
+	}
+
+	&.sys {
+		background: #f97316;
+	}
+}
+
+.node-name {
+	max-width: 120px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+</style>

+ 18 - 74
apps/web/src/nodes/_base/PromptEditor/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed, inject, ref, type Ref } from 'vue'
+import { computed, inject, ref, watch, type Ref } from 'vue'
 import { LexicalComposer } from 'lexical-vue/LexicalComposer'
 import { ContentEditable } from 'lexical-vue/LexicalContentEditable'
 import { HistoryPlugin } from 'lexical-vue/LexicalHistoryPlugin'
@@ -9,7 +9,7 @@ import { $getRoot } from 'lexical'
 import { textToEditorState } from './utils'
 import VarLabelNodePlugin from './plugins/VarLabelBlock.vue'
 import VarLaberPickerPlugin from './plugins/VarLaberPickerPlugin.vue'
-import { VarLabelNode } from './plugins/VarLabel'
+import { syncPromptEditorVarLabelNodeVars, VarLabelNode } from './plugins/VarLabel'
 
 import type { PromptEditorMenuOption } from './plugins/VarLaberPickerPlugin.vue'
 import type { InitialConfigType } from 'lexical-vue'
@@ -40,6 +40,7 @@ const emit = defineEmits<{
 	focus: [event: FocusEvent]
 	blur: [event: FocusEvent]
 }>()
+const contextId = `prompt-editor-${Math.random().toString(36).slice(2, 10)}`
 
 const editorConfig: InitialConfigType = {
 	namespace: 'PromptEditor',
@@ -60,6 +61,17 @@ const editorConfig: InitialConfigType = {
 const isFocused = ref(false)
 const nodeVars = inject<Ref<NodeVar[]>>('nodeVars', ref([]))
 
+watch(
+	nodeVars,
+	(value) => {
+		syncPromptEditorVarLabelNodeVars(contextId, value || [])
+	},
+	{
+		immediate: true,
+		deep: true
+	}
+)
+
 const fallbackMenuOptions = computed<PromptEditorMenuOption[]>(() => {
 	return (nodeVars?.value || []).flatMap((group) => {
 		return group.variableList.map((variable) => {
@@ -128,6 +140,7 @@ function onChange(editorState: any) {
 			class="el-input__wrapper"
 			:class="{ 'is-focus': isFocused, 'is-multiline': isMultiline }"
 			:style="wrapperStyle"
+			:data-prompt-editor-context-id="contextId"
 		>
 			<RichTextPlugin>
 				<template #contentEditable>
@@ -195,85 +208,16 @@ function onChange(editorState: any) {
 	position: relative;
 	top: -1px;
 	line-height: 1;
+	max-width: 100%;
 }
 
-:deep(.var-label-content) {
+:deep(.var-label-token .var-label) {
 	display: inline-flex;
 	align-items: center;
-	height: 22px;
-	padding: 0 8px;
-	padding-left: 0;
-	border-radius: 6px;
-	border: 1px solid transparent;
-	font-size: 14px;
-	line-height: 20px;
-	box-sizing: border-box;
-	gap: 4px;
+	max-width: 100%;
 	vertical-align: middle;
 }
 
-:deep(.env-label .var-label-content) {
-	color: #6366f1;
-	background: #eef2ff;
-	border-color: #c7d2fe;
-}
-
-:deep(.sys-label .var-label-content) {
-	color: #ea580c;
-	background: #fff7ed;
-	border-color: #fed7aa;
-}
-
-:deep(.node-label .var-label-content) {
-	color: #0f766e;
-	background: #f0fdfa;
-	border-color: #99f6e4;
-}
-
-:deep(.custom-label .var-label-content) {
-	color: #334155;
-	background: #f8fafc;
-	border-color: #cbd5e1;
-}
-
-:deep(.var-select__item-prefix) {
-	display: inline-flex;
-	align-items: center;
-	justify-content: center;
-	width: 20px;
-	height: 20px;
-	border-radius: 6px;
-	color: #fff;
-	flex-shrink: 0;
-	line-height: 1;
-	font-weight: 600;
-}
-
-:deep(.var-select__item-prefix.env) {
-	background: #6366f1;
-}
-
-:deep(.var-select__item-prefix.sys) {
-	background: #f97316;
-}
-
-:deep(.var-select__item-prefix.node) {
-	background: #0f766e;
-}
-
-:deep(.var-label-node-name) {
-	max-width: 120px;
-	overflow: hidden;
-	text-overflow: ellipsis;
-	white-space: nowrap;
-}
-
-:deep(.var-label-value),
-:deep(.var-label-separator),
-:deep(.var-label-node-name) {
-	line-height: 20px;
-}
-
 .editor-placeholder {
 	line-height: 30px;
 	color: #a8abb2;

+ 79 - 63
apps/web/src/nodes/_base/PromptEditor/plugins/VarLabel.tsx

@@ -1,5 +1,9 @@
+import { createApp, h, reactive, shallowRef } from 'vue'
 import { TextNode } from 'lexical'
+import type { App, ShallowRef } from 'vue'
 import type { EditorConfig, SerializedTextNode } from 'lexical'
+import VarLabel from '@/components/VarLabel/index.vue'
+import type { NodeVar } from '@/types/var'
 
 export type SerializedVarLabelNode = SerializedTextNode & {
 	type: 'var-label'
@@ -7,90 +11,102 @@ export type SerializedVarLabelNode = SerializedTextNode & {
 	key?: string
 }
 
-type LabelInfo = {
-	type: 'env' | 'sys' | 'node' | 'custom'
-	value: string
-	nodeName?: string
+type MountedVarLabelState = {
+	app: App<Element>
+	props: {
+		label: string
+		contextId: string
+	}
 }
 
-function parseLabelInfo(rawText: string, className: string): LabelInfo {
-	if (rawText.startsWith('#{') && rawText.endsWith('}')) {
-		const expression = rawText.slice(2, -1)
-		const [prefix, ...rest] = expression.split('.')
-		const value = rest.join('.')
+const mountedVarLabels = new WeakMap<HTMLElement, MountedVarLabelState>()
+const promptEditorNodeVarsRefs = new Map<string, ShallowRef<NodeVar[]>>()
+const emptyNodeVars: NodeVar[] = []
 
-		if (prefix === 'env' && value) {
-			return { type: 'env', value }
-		}
+function getNodeVarsRef(contextId: string) {
+	const existingRef = promptEditorNodeVarsRefs.get(contextId)
+	if (existingRef) return existingRef
 
-		if (prefix === 'sys' && value) {
-			return { type: 'sys', value }
-		}
+	const nextRef = shallowRef<NodeVar[]>([])
+	promptEditorNodeVarsRefs.set(contextId, nextRef)
+	return nextRef
+}
 
-		if (prefix && value) {
-			return {
-				type: 'node',
-				nodeName: prefix,
-				value
+function syncMountedContextId(dom: HTMLElement, props: MountedVarLabelState['props'], attempt = 0) {
+	queueMicrotask(() => {
+		if (!dom.isConnected) {
+			if (attempt < 10) {
+				requestAnimationFrame(() => {
+					syncMountedContextId(dom, props, attempt + 1)
+				})
 			}
+			return
 		}
-	}
 
-	if (className.includes('env-label')) {
-		return { type: 'env', value: rawText }
-	}
+		const nextContextId =
+			dom
+				.closest<HTMLElement>('[data-prompt-editor-context-id]')
+				?.getAttribute('data-prompt-editor-context-id') || ''
 
-	if (className.includes('sys-label')) {
-		return { type: 'sys', value: rawText }
-	}
+		if (props.contextId !== nextContextId) {
+			props.contextId = nextContextId
+		}
+	})
+}
 
-	if (className.includes('node-label')) {
-		return { type: 'node', nodeName: 'node', value: rawText }
-	}
+export function syncPromptEditorVarLabelNodeVars(
+	contextId: string,
+	nodeVars: NodeVar[] | undefined
+) {
+	if (!contextId) return
 
-	return { type: 'custom', value: rawText }
-}
+	const nextNodeVars =
+		nodeVars?.map((item) => ({
+			...item,
+			variableList: [...item.variableList]
+		})) || emptyNodeVars
 
-function createSpan(className: string, text: string) {
-	const span = document.createElement('span')
-	span.className = className
-	span.textContent = text
-	return span
+	getNodeVarsRef(contextId).value = nextNodeVars
 }
 
 function renderLabel(dom: HTMLElement, className: string, rawText: string) {
-	const info = parseLabelInfo(rawText, className)
-
 	dom.className = `${className} var-label-token`
 	dom.contentEditable = 'false'
-	dom.title =
-		info.type === 'node' && info.nodeName ? `${info.nodeName} / ${info.value}` : info.value || rawText
-
-	const container = document.createElement('span')
-	container.className = 'var-label-content'
-
-	if (info.type === 'env' || info.type === 'sys') {
-		const prefix = document.createElement('span')
-		prefix.className = `var-select__item-prefix ${info.type} text-6px`
-		prefix.appendChild(createSpan('', info.type.toUpperCase()))
-		container.appendChild(prefix)
-		container.appendChild(createSpan('var-label-value text-gray-600', info.value))
-		dom.replaceChildren(container)
-		return
-	}
+	dom.removeAttribute('title')
 
-	if (info.type === 'node') {
-		const prefix = createSpan('var-select__item-prefix node text-10px', 'VAR')
-		container.appendChild(prefix)
-		container.appendChild(createSpan('var-label-node-name', info.nodeName || 'node'))
-		container.appendChild(createSpan('var-label-separator mx-2px text-gray-400', '/'))
-		container.appendChild(createSpan('var-label-value text-gray-600', info.value))
-		dom.replaceChildren(container)
+	const mountedState = mountedVarLabels.get(dom)
+	if (mountedState) {
+		mountedState.props.label = rawText
+		syncMountedContextId(dom, mountedState.props)
 		return
 	}
 
-	container.appendChild(createSpan('var-label-value text-gray-600', info.value))
-	dom.replaceChildren(container)
+	const mountPoint = document.createElement('span')
+	dom.replaceChildren(mountPoint)
+
+	const props = reactive({
+		label: rawText,
+		contextId: ''
+	})
+
+	const app = createApp({
+		render() {
+			const nodeVars = props.contextId ? getNodeVarsRef(props.contextId).value : emptyNodeVars
+
+			return h(VarLabel, {
+				label: props.label,
+				nodeVars
+			})
+		}
+	})
+
+	app.mount(mountPoint)
+	syncMountedContextId(dom, props)
+
+	mountedVarLabels.set(dom, {
+		app,
+		props
+	})
 }
 
 export class VarLabelNode extends TextNode {

+ 53 - 29
apps/web/src/nodes/_base/PromptEditor/plugins/VarLaberPickerPlugin.vue

@@ -4,7 +4,13 @@ import { MenuOption } from 'lexical-vue'
 import { useLexicalComposer } from 'lexical-vue/LexicalComposer'
 import { TypeaheadMenuPlugin } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
 import type { TriggerFn } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
-import { $insertNodes, type LexicalEditor, type TextNode } from 'lexical'
+import {
+	$getSelection,
+	$insertNodes,
+	$isRangeSelection,
+	type LexicalEditor,
+	type TextNode
+} from 'lexical'
 import { Icon } from '@repo/ui'
 import { VARIABLE_TYPE_OPTIONS } from '@/constant'
 import { nodeMap } from '@/nodes'
@@ -75,9 +81,10 @@ const props = withDefaults(
 
 const editor = useLexicalComposer()
 const { t } = useI18n()
-const keyword = ref('')
+const queryKeyword = ref('')
+const panelKeyword = ref('')
 const isMenuOpen = ref(false)
-const suppressMenu = ref(false)
+const suppressedText = ref<string | null>(null)
 const menuPanelRef = ref<HTMLElement | null>(null)
 
 const normalizedOptions = computed(() => {
@@ -85,14 +92,19 @@ const normalizedOptions = computed(() => {
 })
 
 const filteredOptions = computed(() => {
-	const kw = keyword.value.trim().toLowerCase()
-	if (!kw) return normalizedOptions.value
+	const query = queryKeyword.value.trim().toLowerCase()
+	const panel = panelKeyword.value.trim().toLowerCase()
+	if (!query && !panel) return normalizedOptions.value
 
 	return normalizedOptions.value.filter((option) => {
 		const label = option.label.toLowerCase()
 		const value = option.value.toLowerCase()
 		const groupName = (option.groupName || '').toLowerCase()
-		return label.includes(kw) || value.includes(kw) || groupName.includes(kw)
+
+		return [query, panel].every((keyword) => {
+			if (!keyword) return true
+			return label.includes(keyword) || value.includes(keyword) || groupName.includes(keyword)
+		})
 	})
 })
 
@@ -119,16 +131,31 @@ const normalizedTriggers = computed(() => {
 		.filter((trigger) => trigger.length > 0)
 })
 
-const hasFreshTriggerAtTail = (text: string) => {
-	return normalizedTriggers.value.some((trigger) => text.endsWith(trigger))
+const getTextUpToAnchor = () => {
+	let text: string | null = null
+
+	editor.getEditorState().read(() => {
+		const selection = $getSelection()
+		if (!$isRangeSelection(selection) || !selection.isCollapsed()) return
+
+		const anchor = selection.anchor
+		if (anchor.type !== 'text') return
+
+		const anchorNode = anchor.getNode()
+		if (!anchorNode.isSimpleText()) return
+
+		text = anchorNode.getTextContent().slice(0, anchor.offset)
+	})
+
+	return text
 }
 
 const triggerMatch: TriggerFn = (text, _lexicalEditor: LexicalEditor) => {
-	if (suppressMenu.value) {
-		if (!hasFreshTriggerAtTail(text)) {
+	if (suppressedText.value !== null) {
+		if (suppressedText.value === text) {
 			return null
 		}
-		suppressMenu.value = false
+		suppressedText.value = null
 	}
 
 	let latestMatch: ReturnType<TriggerFn> = null
@@ -172,13 +199,7 @@ const normalizeTypeLabel = (type?: string) => {
 }
 
 const onQueryChange = (payload: string | null) => {
-	const nextKeyword = payload || ''
-	if (nextKeyword.length > 0) {
-		requestCloseMenu()
-		return
-	}
-
-	keyword.value = nextKeyword
+	queryKeyword.value = payload || ''
 }
 
 const onOpen = () => {
@@ -187,12 +208,14 @@ const onOpen = () => {
 
 const onClose = () => {
 	isMenuOpen.value = false
-	keyword.value = ''
+	queryKeyword.value = ''
+	panelKeyword.value = ''
 }
 
 const requestCloseMenu = () => {
-	suppressMenu.value = true
-	keyword.value = ''
+	suppressedText.value = getTextUpToAnchor()
+	queryKeyword.value = ''
+	panelKeyword.value = ''
 	editor.update(() => {
 		// Trigger plugin update cycle so menu closes immediately.
 	})
@@ -239,11 +262,11 @@ const onDocumentMouseDown = (event: MouseEvent) => {
 	if (clickedInsideMenu) return
 
 	const rootElement = editor.getRootElement()
-	const clickedInsideEditor = rootElement?.contains(target) ?? false
-	if (clickedInsideEditor) return
-
 	requestCloseMenu()
-	rootElement?.blur()
+	const clickedInsideEditor = rootElement?.contains(target) ?? false
+	if (!clickedInsideEditor) {
+		rootElement?.blur()
+	}
 }
 
 const onEditorKeyDown = (event: KeyboardEvent) => {
@@ -288,13 +311,14 @@ onUnmounted(() => {
 				<div ref="menuPanelRef" class="var-select__popover">
 					<div class="var-select__panel">
 						<el-input
-							:model-value="keyword"
+							v-model="panelKeyword"
+							clearable
 							size="small"
 							:placeholder="t('common.nodeBase.promptEditor.searchVariable')"
 							class="var-select__search"
-							readonly
-							tabindex="-1"
-							@mousedown.prevent
+							@mousedown.stop
+							@click.stop
+							@keydown.stop
 						/>
 
 						<div class="var-select__list">

+ 2 - 87
apps/web/src/nodes/_base/VarSelect.vue

@@ -20,33 +20,7 @@
 						v-bind="$attrs"
 					>
 						<template #tag>
-							<div v-if="valueInfo.type === 'env'" class="flex gap-1 items-center truncate">
-								<span class="var-select__item-prefix env text-6px">
-									<span>ENV</span>
-								</span>
-								<span class="text-gray-600">{{ valueInfo.value }}</span>
-							</div>
-							<div v-else-if="valueInfo.type === 'sys'" class="flex gap-1 items-center truncate">
-								<span class="var-select__item-prefix sys text-6px">
-									<span>SYS</span>
-								</span>
-								<span class="text-gray-600">{{ valueInfo.value }}</span>
-							</div>
-							<div v-else class="truncate" :title="valueInfo.nodeName + ' / ' + valueInfo.value">
-								<span
-									class="var-select__item-prefix env text-10px"
-									:style="{ background: nodeMap[valueInfo?.nodeType ?? '']?.iconColor ?? '' }"
-								>
-									<Icon
-										v-if="nodeMap[valueInfo?.nodeType!]?.icon"
-										:icon="nodeMap[valueInfo?.nodeType!]?.icon!"
-										:size="18"
-									/>
-								</span>
-								<span>{{ valueInfo.nodeName }}</span>
-								<span class="mx-2px text-gray-400">/</span>
-								<span class="text-gray-600">{{ valueInfo.value }}</span>
-							</div>
+							<VarLabel :label="innerValue" />
 						</template>
 					</el-input-tag>
 				</div>
@@ -104,6 +78,7 @@ import { Icon } from '@repo/ui'
 import { nodeMap } from '@/nodes'
 import { VARIABLE_TYPE_OPTIONS } from '@/constant'
 import { useI18n } from '@/composables/useI18n'
+import VarLabel from '@/components/VarLabel/index.vue'
 
 import type { NodeVar, VarType } from '@/types/var'
 
@@ -171,66 +146,6 @@ watch(
 		emit('update:varType', val)
 	}
 )
-
-const valueInfo = computed(() => {
-	// 根据#{xxx.var}解析变量
-	// 解析格式 #{env.xxx} 或 #{nodeId.xxx}
-	if (!innerValue.value?.startsWith('#{') || !innerValue.value?.endsWith('}')) {
-		return {
-			type: 'env',
-			name: innerValue.value || '',
-			value: innerValue.value || ''
-		}
-	}
-
-	const expr = innerValue.value.slice(2, -1) // 去掉 #{ 和 }
-	const [prefix, ...rest] = expr.split('.')
-	const varName = rest.join('.')
-
-	if (!prefix || !varName) {
-		return {
-			type: 'env',
-			name: innerValue.value || '',
-			value: innerValue.value || ''
-		}
-	}
-
-	if (prefix === 'env') {
-		// 环境变量
-		return {
-			type: 'env',
-			name: 'env',
-			value: varName
-		}
-	} else if (prefix.startsWith('sys')) {
-		// 系统变量
-		return {
-			type: 'sys',
-			name: 'sys',
-			value: varName
-		}
-	} else {
-		// 节点变量,需要解析节点类型名称
-		if (nodeVars && Array.isArray(nodeVars.value)) {
-			const node = nodeVars.value.find((item) => item.id === prefix)
-			return {
-				type: 'node',
-				nodeType: node?.type || '',
-				nodeName: node?.name || prefix,
-				name: varName,
-				value: varName
-			}
-		} else {
-			return {
-				type: 'node',
-				nodeType: '',
-				nodeName: prefix,
-				name: varName,
-				value: varName
-			}
-		}
-	}
-})
 const normalizeTypeLabel = (type?: VarType) => {
 	if (!type) return ''
 	const labelMap: Record<string, string> = {

+ 16 - 14
apps/web/src/views/editor/PublishBtn.vue

@@ -58,24 +58,26 @@ const { t } = useI18n()
 const popoverRef = ref<PopoverInstance>()
 const isPublishing = ref(false)
 
-const PUBLISHABLE_NODE_TYPES = ['start', 'trigger-schedule', 'trigger-webhook']
+// const PUBLISHABLE_NODE_TYPES = ['start', 'trigger-schedule', 'trigger-webhook']
 
 const isPublished = computed(() => props.workflow?.status === 'published')
 
 const publishNodes = computed(() => {
-	return (props.workflow?.nodes || [])
-		.filter((node) => {
-			const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
-			return PUBLISHABLE_NODE_TYPES.includes(nodeType || '')
-		})
-		.map((node) => {
-			const nodeType = ((node as any)?.nodeType || (node as any)?.data?.nodeType || '') as string
-			return {
-				id: node.id,
-				name: node.name || node.data?.title || getNodeDisplayName(nodeType) || nodeType,
-				nodeType
-			}
-		})
+	return (
+		(props.workflow?.nodes || [])
+			// .filter((node) => {
+			// 	const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
+			// 	return PUBLISHABLE_NODE_TYPES.includes(nodeType || '')
+			// })
+			.map((node) => {
+				const nodeType = ((node as any)?.nodeType || (node as any)?.data?.nodeType || '') as string
+				return {
+					id: node.id,
+					name: node.name || node.data?.title || getNodeDisplayName(nodeType) || nodeType,
+					nodeType
+				}
+			})
+	)
 })
 
 const publishAgent = async (nodeId?: string) => {

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

@@ -80,12 +80,12 @@ export default defineConfig(({ mode }) => {
 					rewrite: (path) => path.replace(/^\/api/, '/api')
 				},
 				'/Content': {
-					target: `http://${env.VITE_BASE_URL}`,
+					target: `http://shalu-componenttesting-admin-dev.shalu.com`,
 					changeOrigin: true,
 					rewrite: (path) => path.replace(/^\/Content/, '/Content')
 				},
 				'/Views': {
-					target: `http://${env.VITE_BASE_URL}`,
+					target: `http://shalu-componenttesting-admin-dev.shalu.com`,
 					changeOrigin: true,
 					rewrite: (path) => path.replace(/^\/Views/, '/Views')
 				},

+ 97 - 0
packages/api-service/agent.openapi.json

@@ -5136,6 +5136,103 @@
 				},
 				"security": []
 			}
+		},
+		"/api/agent/getSupportCustomAgentNodeYamlList": {
+			"post": {
+				"summary": "获取支持的自定义智能体节点yaml列表",
+				"deprecated": false,
+				"description": "",
+				"tags": ["Agent"],
+				"parameters": [
+					{
+						"name": "Authorization",
+						"in": "header",
+						"description": "",
+						"example": "bpm_client_1500870842102321152",
+						"schema": {
+							"type": "string",
+							"default": "bpm_client_1500870842102321152"
+						}
+					}
+				],
+				"requestBody": {
+					"content": {
+						"application/json": {
+							"schema": {
+								"type": "object",
+								"properties": {}
+							},
+							"example": {}
+						}
+					},
+					"required": true
+				},
+				"responses": {
+					"200": {
+						"description": "",
+						"content": {
+							"application/json": {
+								"schema": {
+									"type": "object",
+									"properties": {
+										"isSuccess": {
+											"type": "boolean"
+										},
+										"code": {
+											"type": "integer"
+										},
+										"result": {
+											"type": "array",
+											"items": {
+												"type": "string"
+											}
+										},
+										"isAuthorized": {
+											"type": "boolean"
+										}
+									},
+									"required": ["isSuccess", "code", "result", "isAuthorized"]
+								},
+								"example": {
+									"isSuccess": true,
+									"code": 1,
+									"result": [
+										{
+											"identity": {
+												"author": "lyh",
+												"description": "test1 desc",
+												"icon24": "",
+												"icon40": "",
+												"label": "test1",
+												"name": "test1",
+												"type": "custom-module-AgentNodeTest1"
+											},
+											"parameters": [
+												{
+													"defaultValue": "",
+													"description": "",
+													"label": "name",
+													"llm_description": "",
+													"max": 3.4028234663852886e38,
+													"min": 0,
+													"name": "name",
+													"options": [],
+													"placeholder": "",
+													"required": false,
+													"type": "text-input"
+												}
+											]
+										}
+									],
+									"isAuthorized": true
+								}
+							}
+						},
+						"headers": {}
+					}
+				},
+				"security": []
+			}
 		}
 	},
 	"components": {

+ 18 - 0
packages/api-service/servers/api/agent.ts

@@ -513,3 +513,21 @@ export async function postAgentGetPrevNodeOutVariableList(
     ...(options || {})
   })
 }
+
+/** 获取支持的自定义智能体节点yaml列表 POST /api/agent/getSupportCustomAgentNodeYamlList */
+export async function postAgentGetSupportCustomAgentNodeYamlList(
+  body: {},
+  options?: { [key: string]: any }
+) {
+  return request<{ isSuccess: boolean; code: number; result: string[]; isAuthorized: boolean }>(
+    '/api/agent/getSupportCustomAgentNodeYamlList',
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      data: body,
+      ...(options || {})
+    }
+  )
+}