mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
fix(workflow): tighten tsgo types in workflow editor
This commit is contained in:
@ -20,7 +20,7 @@ const createModel = (overrides: Partial<ModelConfig> = {}): ModelConfig => ({
|
||||
provider: 'openai',
|
||||
name: 'gpt-4o',
|
||||
mode: 'chat',
|
||||
completion_params: [],
|
||||
completion_params: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
||||
@ -1963,7 +1963,7 @@ export const useNodesInteractions = () => {
|
||||
if (selectedNode) {
|
||||
// Keep this list aligned with availableBlocksFilter(inContainer)
|
||||
// in use-available-blocks.ts.
|
||||
const commonNestedDisallowPasteNodes = [
|
||||
const commonNestedDisallowPasteNodes: BlockEnum[] = [
|
||||
BlockEnum.End,
|
||||
BlockEnum.Iteration,
|
||||
BlockEnum.Loop,
|
||||
|
||||
@ -158,16 +158,28 @@ export const getTargetVarType = (state: FormInputState) => {
|
||||
export const getFilterVar = (state: FormInputState) => {
|
||||
if (state.isNumber)
|
||||
return (varPayload: Var) => varPayload.type === VarType.number
|
||||
if (state.isString)
|
||||
return (varPayload: Var) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
if (state.isString) {
|
||||
return (varPayload: Var) => (
|
||||
varPayload.type === VarType.string
|
||||
|| varPayload.type === VarType.number
|
||||
|| varPayload.type === VarType.secret
|
||||
)
|
||||
}
|
||||
if (state.isFile)
|
||||
return (varPayload: Var) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
return (varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
|
||||
if (state.isBoolean)
|
||||
return (varPayload: Var) => varPayload.type === VarType.boolean
|
||||
if (state.isObject)
|
||||
return (varPayload: Var) => varPayload.type === VarType.object
|
||||
if (state.isArray)
|
||||
return (varPayload: Var) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayMessage].includes(varPayload.type)
|
||||
if (state.isArray) {
|
||||
return (varPayload: Var) => (
|
||||
varPayload.type === VarType.array
|
||||
|| varPayload.type === VarType.arrayString
|
||||
|| varPayload.type === VarType.arrayNumber
|
||||
|| varPayload.type === VarType.arrayObject
|
||||
|| varPayload.type === VarType.arrayMessage
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@ -308,6 +308,7 @@ const FormInputItem: FC<Props> = ({
|
||||
const resolvedType = isAssembleValue
|
||||
? VarKindType.nested_node
|
||||
: newType ?? (varInput?.type === VarKindType.nested_node ? VarKindType.nested_node : getVarKindType(formState))
|
||||
const nextVarKindType = resolvedType ?? varInput?.type ?? VarKindType.constant
|
||||
const resolvedNestedNodeConfig = resolvedType === VarKindType.nested_node
|
||||
? (nestedNodeConfig ?? varInput?.nested_node_config ?? {
|
||||
extractor_node_id: nodeId && variable ? `${nodeId}_ext_${variable}` : '',
|
||||
@ -321,7 +322,7 @@ const FormInputItem: FC<Props> = ({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: resolvedType,
|
||||
type: nextVarKindType,
|
||||
value: normalizedValue,
|
||||
nested_node_config: resolvedNestedNodeConfig,
|
||||
},
|
||||
|
||||
@ -19,6 +19,7 @@ const createProps = (
|
||||
isLoading: false,
|
||||
isShowAPart: false,
|
||||
isShowNodeName: true,
|
||||
isValidVar: true,
|
||||
maxNodeNameWidth: 80,
|
||||
maxTypeWidth: 60,
|
||||
maxVarNameWidth: 80,
|
||||
|
||||
@ -144,7 +144,11 @@ const Item: FC<ItemProps> = ({
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0)
|
||||
const isObj = (
|
||||
(itemData.type === VarType.object || itemData.type === VarType.file)
|
||||
&& itemData.children
|
||||
&& (itemData.children as Var[]).length > 0
|
||||
)
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
|
||||
@ -37,7 +37,7 @@ const createInputVar = (variable: string): InputVar => ({
|
||||
required: false,
|
||||
})
|
||||
|
||||
const createNode = (id: string, title: string, type = BlockEnum.Tool): Node => ({
|
||||
const createNode = (id: string, title: string, type: BlockEnum = BlockEnum.Tool): Node => ({
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ChatVarType } from '../type'
|
||||
import type { ConversationVariable } from '@/app/components/workflow/types'
|
||||
import type { ConversationVariable, JsonValue } from '@/app/components/workflow/types'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { ChatVarType as ChatVarTypeEnum } from '../type'
|
||||
import {
|
||||
@ -29,6 +29,31 @@ export type ToastPayload = {
|
||||
customComponent?: ReactNode
|
||||
}
|
||||
|
||||
const isJsonValue = (value: unknown): value is JsonValue => {
|
||||
if (value === null)
|
||||
return true
|
||||
|
||||
const valueType = typeof value
|
||||
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean')
|
||||
return true
|
||||
|
||||
if (Array.isArray(value))
|
||||
return value.every(item => isJsonValue(item))
|
||||
|
||||
if (valueType === 'object') {
|
||||
if (!value)
|
||||
return false
|
||||
|
||||
return Object.values(value).every(item => isJsonValue(item))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const isJsonRecord = (value: JsonValue): value is Record<string, JsonValue> => {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export const typeList = [
|
||||
ChatVarTypeEnum.String,
|
||||
ChatVarTypeEnum.Number,
|
||||
@ -56,23 +81,27 @@ export const getPlaceholderByType = (type: ChatVarType) => {
|
||||
}
|
||||
|
||||
export const buildObjectValueItems = (chatVar?: ConversationVariable): ObjectValueItem[] => {
|
||||
if (!chatVar || !chatVar.value || Object.keys(chatVar.value).length === 0)
|
||||
if (!chatVar)
|
||||
return [DEFAULT_OBJECT_VALUE]
|
||||
|
||||
return Object.keys(chatVar.value).map((key) => {
|
||||
const itemValue = chatVar.value[key]
|
||||
const value = chatVar.value
|
||||
if (!isJsonRecord(value) || Object.keys(value).length === 0)
|
||||
return [DEFAULT_OBJECT_VALUE]
|
||||
|
||||
return Object.keys(value).map((key) => {
|
||||
const itemValue = value[key]
|
||||
return {
|
||||
key,
|
||||
type: typeof itemValue === 'string' ? ChatVarTypeEnum.String : ChatVarTypeEnum.Number,
|
||||
value: itemValue,
|
||||
value: typeof itemValue === 'string' || typeof itemValue === 'number' ? itemValue : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const formatObjectValueFromList = (list: ObjectValueItem[]) => {
|
||||
return list.reduce<Record<string, string | number | null>>((acc, curr) => {
|
||||
return list.reduce<Record<string, JsonValue>>((acc, curr) => {
|
||||
if (curr.key)
|
||||
acc[curr.key] = curr.value || null
|
||||
acc[curr.key] = curr.value === undefined || curr.value === '' ? null : curr.value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
@ -87,22 +116,22 @@ export const formatChatVariableValue = ({
|
||||
objectValue: ObjectValueItem[]
|
||||
type: ChatVarType
|
||||
value: unknown
|
||||
}) => {
|
||||
}): JsonValue => {
|
||||
switch (type) {
|
||||
case ChatVarTypeEnum.String:
|
||||
return value || ''
|
||||
return typeof value === 'string' ? value : ''
|
||||
case ChatVarTypeEnum.Number:
|
||||
return value || 0
|
||||
return typeof value === 'number' && !Number.isNaN(value) ? value : 0
|
||||
case ChatVarTypeEnum.Boolean:
|
||||
return value === undefined ? true : value
|
||||
return typeof value === 'boolean' ? value : false
|
||||
case ChatVarTypeEnum.Object:
|
||||
return editInJSON ? value : formatObjectValueFromList(objectValue)
|
||||
return editInJSON && isJsonValue(value) ? value : formatObjectValueFromList(objectValue)
|
||||
case ChatVarTypeEnum.ArrayString:
|
||||
case ChatVarTypeEnum.ArrayNumber:
|
||||
case ChatVarTypeEnum.ArrayObject:
|
||||
return Array.isArray(value) ? value.filter(Boolean) : []
|
||||
return Array.isArray(value) ? value.filter((item): item is JsonValue => item !== undefined) : []
|
||||
case ChatVarTypeEnum.ArrayBoolean:
|
||||
return value || []
|
||||
return Array.isArray(value) ? value.filter((item): item is JsonValue => item !== undefined) : []
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,10 +175,16 @@ export const parseEditorContent = ({
|
||||
}: {
|
||||
content: string
|
||||
type: ChatVarType
|
||||
}) => {
|
||||
const parsed = JSON.parse(content)
|
||||
if (type !== ChatVarTypeEnum.ArrayBoolean)
|
||||
}): JsonValue => {
|
||||
const parsed: unknown = JSON.parse(content)
|
||||
if (type !== ChatVarTypeEnum.ArrayBoolean) {
|
||||
if (!isJsonValue(parsed))
|
||||
throw new Error('Invalid JSON')
|
||||
return parsed
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed))
|
||||
throw new Error('Invalid JSON array')
|
||||
|
||||
return parsed
|
||||
.map((item: string | boolean) => {
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import type { CommonNodeType, Node } from './types'
|
||||
import type {
|
||||
AnnotationReplyConfig,
|
||||
FileUpload,
|
||||
OpeningStatement,
|
||||
RetrieverResource,
|
||||
Runtime,
|
||||
SensitiveWordAvoidance,
|
||||
SpeechToText,
|
||||
SuggestedQuestionsAfterAnswer,
|
||||
TextToSpeech,
|
||||
} from '@/app/components/base/features/types'
|
||||
import type { WorkflowDraftFeatures } from '@/types/workflow'
|
||||
import { load as yamlLoad } from 'js-yaml'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AppModeEnum, TransferMethod } from '@/types/app'
|
||||
import { BlockEnum, SupportUploadFileTypes } from './types'
|
||||
|
||||
type ParsedDSL = {
|
||||
@ -13,47 +25,49 @@ type ParsedDSL = {
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowFileUploadFeatures = {
|
||||
enabled?: boolean
|
||||
allowed_file_types?: SupportUploadFileTypes[]
|
||||
allowed_file_extensions?: string[]
|
||||
allowed_file_upload_methods?: string[]
|
||||
number_limits?: number
|
||||
image?: {
|
||||
enabled?: boolean
|
||||
number_limits?: number
|
||||
transfer_methods?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowFeatures = {
|
||||
file_upload?: WorkflowFileUploadFeatures
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
suggested_questions_after_answer?: { enabled: boolean }
|
||||
speech_to_text?: { enabled: boolean }
|
||||
text_to_speech?: { enabled: boolean }
|
||||
retriever_resource?: { enabled: boolean }
|
||||
sensitive_word_avoidance?: { enabled: boolean }
|
||||
}
|
||||
|
||||
type ImportNotificationPayload = {
|
||||
type: 'success' | 'warning'
|
||||
message: string
|
||||
children?: string
|
||||
}
|
||||
|
||||
type NormalizedWorkflowFeatures = {
|
||||
file: FileUpload
|
||||
opening: OpeningStatement
|
||||
suggested: SuggestedQuestionsAfterAnswer
|
||||
speech2text: SpeechToText
|
||||
text2speech: TextToSpeech
|
||||
citation: RetrieverResource
|
||||
moderation: SensitiveWordAvoidance
|
||||
annotationReply: AnnotationReplyConfig
|
||||
sandbox: Runtime
|
||||
}
|
||||
|
||||
const DEFAULT_TRANSFER_METHODS: TransferMethod[] = [TransferMethod.local_file, TransferMethod.remote_url]
|
||||
|
||||
const normalizeEnabledFeature = (feature?: boolean | Record<string, unknown>) => {
|
||||
if (typeof feature === 'boolean')
|
||||
return { enabled: feature }
|
||||
|
||||
if (feature && typeof feature === 'object')
|
||||
return { enabled: typeof feature.enabled === 'boolean' ? feature.enabled : false }
|
||||
|
||||
return { enabled: false }
|
||||
}
|
||||
|
||||
export const getInvalidNodeTypes = (mode?: AppModeEnum) => {
|
||||
if (mode === AppModeEnum.ADVANCED_CHAT) {
|
||||
return [
|
||||
const invalidNodeTypes: BlockEnum[] = [
|
||||
BlockEnum.End,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
return invalidNodeTypes
|
||||
}
|
||||
|
||||
return [BlockEnum.Answer]
|
||||
const invalidNodeTypes: BlockEnum[] = [BlockEnum.Answer]
|
||||
return invalidNodeTypes
|
||||
}
|
||||
|
||||
export const validateDSLContent = (content: string, mode?: AppModeEnum) => {
|
||||
@ -61,7 +75,7 @@ export const validateDSLContent = (content: string, mode?: AppModeEnum) => {
|
||||
const data = yamlLoad(content) as ParsedDSL
|
||||
const nodes = data?.workflow?.graph?.nodes ?? []
|
||||
const invalidNodes = getInvalidNodeTypes(mode)
|
||||
return !nodes.some((node: Node<CommonNodeType>) => invalidNodes.includes(node?.data?.type))
|
||||
return !nodes.some((node: Node<CommonNodeType>) => node?.data?.type ? invalidNodes.includes(node.data.type) : false)
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
@ -82,19 +96,19 @@ export const getImportNotificationPayload = (status: DSLImportStatus, t: (key: s
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeWorkflowFeatures = (features?: WorkflowFeatures) => {
|
||||
export const normalizeWorkflowFeatures = (features?: WorkflowDraftFeatures): NormalizedWorkflowFeatures => {
|
||||
const resolvedFeatures = features ?? {}
|
||||
return {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!resolvedFeatures.file_upload?.image?.enabled,
|
||||
number_limits: resolvedFeatures.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: resolvedFeatures.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
transfer_methods: resolvedFeatures.file_upload?.image?.transfer_methods || DEFAULT_TRANSFER_METHODS,
|
||||
},
|
||||
enabled: !!(resolvedFeatures.file_upload?.enabled || resolvedFeatures.file_upload?.image?.enabled),
|
||||
allowed_file_types: resolvedFeatures.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: resolvedFeatures.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: resolvedFeatures.file_upload?.allowed_file_upload_methods || resolvedFeatures.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
allowed_file_upload_methods: resolvedFeatures.file_upload?.allowed_file_upload_methods || resolvedFeatures.file_upload?.image?.transfer_methods || DEFAULT_TRANSFER_METHODS,
|
||||
number_limits: resolvedFeatures.file_upload?.number_limits || resolvedFeatures.file_upload?.image?.number_limits || 3,
|
||||
},
|
||||
opening: {
|
||||
@ -102,10 +116,12 @@ export const normalizeWorkflowFeatures = (features?: WorkflowFeatures) => {
|
||||
opening_statement: resolvedFeatures.opening_statement,
|
||||
suggested_questions: resolvedFeatures.suggested_questions,
|
||||
},
|
||||
suggested: resolvedFeatures.suggested_questions_after_answer || { enabled: false },
|
||||
speech2text: resolvedFeatures.speech_to_text || { enabled: false },
|
||||
text2speech: resolvedFeatures.text_to_speech || { enabled: false },
|
||||
citation: resolvedFeatures.retriever_resource || { enabled: false },
|
||||
moderation: resolvedFeatures.sensitive_word_avoidance || { enabled: false },
|
||||
suggested: normalizeEnabledFeature(resolvedFeatures.suggested_questions_after_answer),
|
||||
speech2text: normalizeEnabledFeature(resolvedFeatures.speech_to_text),
|
||||
text2speech: normalizeEnabledFeature(resolvedFeatures.text_to_speech),
|
||||
citation: normalizeEnabledFeature(resolvedFeatures.retriever_resource),
|
||||
moderation: normalizeEnabledFeature(resolvedFeatures.sensitive_word_avoidance),
|
||||
annotationReply: normalizeEnabledFeature(resolvedFeatures.annotation_reply),
|
||||
sandbox: normalizeEnabledFeature(resolvedFeatures.sandbox),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import type { FileResponse, VarInInspect } from '@/types/workflow'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
@ -14,6 +14,14 @@ type UploadedFileLike = {
|
||||
upload_file_id?: string
|
||||
}
|
||||
|
||||
const isFileResponse = (value: unknown): value is FileResponse => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
return false
|
||||
|
||||
return ['related_id', 'filename', 'mime_type', 'transfer_method', 'type', 'url', 'upload_file_id', 'remote_url']
|
||||
.every(key => key in value)
|
||||
}
|
||||
|
||||
export const getValueEditorState = (currentVar: VarInInspect) => {
|
||||
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
|
||||
const showBoolEditor = typeof currentVar.value === 'boolean'
|
||||
@ -40,9 +48,15 @@ export const getValueEditorState = (currentVar: VarInInspect) => {
|
||||
|
||||
export const formatInspectFileValue = (currentVar: VarInInspect) => {
|
||||
if (currentVar.value_type === 'file')
|
||||
return currentVar.value ? getProcessedFilesFromResponse([currentVar.value]) : []
|
||||
if (currentVar.value_type === 'array[file]' || (currentVar.type === VarInInspectType.system && currentVar.name === 'files'))
|
||||
return currentVar.value && currentVar.value.length > 0 ? getProcessedFilesFromResponse(currentVar.value) : []
|
||||
return isFileResponse(currentVar.value) ? getProcessedFilesFromResponse([currentVar.value]) : []
|
||||
|
||||
if (currentVar.value_type === 'array[file]' || (currentVar.type === VarInInspectType.system && currentVar.name === 'files')) {
|
||||
if (!Array.isArray(currentVar.value) || !currentVar.value.every(isFileResponse))
|
||||
return []
|
||||
|
||||
return currentVar.value.length > 0 ? getProcessedFilesFromResponse(currentVar.value) : []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user