feat: improve output node (#35511)

This commit is contained in:
非法操作 2026-06-05 10:14:23 +08:00 committed by GitHub
parent a8f009a965
commit 8cb2cffbf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 182 additions and 9 deletions

View File

@ -81,9 +81,10 @@ describe('useAvailableBlocks', () => {
expect(result.current.availableNextBlocks).toEqual([])
})
it('should return empty array for End node', () => {
it('should return available nodes for End node', () => {
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.End), { hooksStoreProps })
expect(result.current.availableNextBlocks).toEqual([])
expect(result.current.availableNextBlocks.length).toBeGreaterThan(0)
expect(result.current.availableNextBlocks).toContain(BlockEnum.Code)
})
it('should return empty array for LoopEnd node', () => {
@ -143,10 +144,11 @@ describe('useAvailableBlocks', () => {
expect(blocks.availablePrevBlocks).toEqual([])
})
it('should return empty nextBlocks for End/LoopEnd/KnowledgeBase', () => {
it('should return empty nextBlocks for LoopEnd/KnowledgeBase and available nodes for End', () => {
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
expect(result.current.getAvailableBlocks(BlockEnum.End).availableNextBlocks).toEqual([])
expect(result.current.getAvailableBlocks(BlockEnum.End).availableNextBlocks.length).toBeGreaterThan(0)
expect(result.current.getAvailableBlocks(BlockEnum.End).availableNextBlocks).toContain(BlockEnum.Code)
expect(result.current.getAvailableBlocks(BlockEnum.LoopEnd).availableNextBlocks).toEqual([])
expect(result.current.getAvailableBlocks(BlockEnum.KnowledgeBase).availableNextBlocks).toEqual([])
})

View File

@ -372,6 +372,41 @@ describe('useChecklist', () => {
])
})
it('should detect duplicate output variables across end nodes', () => {
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
const firstEndNode = createNode({
id: 'end-1',
data: {
type: BlockEnum.End,
title: 'Output 1',
outputs: [{ variable: 'workflow_id', value_selector: ['sys', 'workflow_id'] }],
},
})
const secondEndNode = createNode({
id: 'end-2',
data: {
type: BlockEnum.End,
title: 'Output 2',
outputs: [{ variable: 'workflow_id', value_selector: ['sys', 'workflow_id'] }],
},
})
const edges = [
createEdge({ source: 'start', target: 'end-1' }),
createEdge({ source: 'start', target: 'end-2' }),
]
const { result } = renderWorkflowHook(
() => useChecklist([startNode, firstEndNode, secondEndNode], edges),
)
const firstWarning = result.current.find((item: ChecklistItem) => item.id === 'end-1')
const secondWarning = result.current.find((item: ChecklistItem) => item.id === 'end-2')
expect(firstWarning?.errorMessages.some(message => message.includes('duplicateOutputVariable'))).toBe(true)
expect(secondWarning?.errorMessages.some(message => message.includes('duplicateOutputVariable'))).toBe(true)
})
it('should sync checklist items to the workflow store without render phase update warnings', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
try {

View File

@ -30,7 +30,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
return availableNodesType
}, [availableNodesType, nodeType])
const availableNextBlocks = useMemo(() => {
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
return []
return availableNodesType
@ -42,7 +42,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
availablePrevBlocks = []
let availableNextBlocks = availableNodesType
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
availableNextBlocks = []
return {

View File

@ -91,6 +91,43 @@ const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.TriggerPlugin,
]
const getDuplicateEndOutputMessages = (
nodes: Node[],
t: ReturnType<typeof useTranslation>['t'],
) => {
const variableOccurrences = new Map<string, string[]>()
nodes.forEach((node) => {
if (node.type !== CUSTOM_NODE || node.data.type !== BlockEnum.End)
return
const outputs = ((node.data as { outputs?: Array<{ variable?: string }> }).outputs) || []
outputs.forEach((output) => {
const variable = output.variable?.trim()
if (!variable)
return
const occurrences = variableOccurrences.get(variable) || []
occurrences.push(node.id)
variableOccurrences.set(variable, occurrences)
})
})
const nodeMessages = new Map<string, string[]>()
variableOccurrences.forEach((nodeIds, variable) => {
if (nodeIds.length <= 1)
return
Array.from(new Set(nodeIds)).forEach((nodeId) => {
const messages = nodeMessages.get(nodeId) || []
messages.push(t('errorMsg.duplicateOutputVariable', { ns: 'workflow', variable }))
nodeMessages.set(nodeId, messages)
})
})
return nodeMessages
}
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
const language = useGetLanguage()
@ -179,6 +216,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const needWarningNodes = useMemo<ChecklistItem[]>(() => {
const list: ChecklistItem[] = []
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t)
const { validNodes } = getValidTreeNodes(filteredNodes, edges)
const installedPluginIds = new Set(modelProviders.map(p => extractPluginId(p.provider)))
@ -253,6 +291,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
if (hasInvalidVar)
errorMessages.push(t('errorMsg.invalidVariable', { ns: 'workflow' }))
errorMessages.push(...(duplicateEndOutputMessages.get(node!.id) || []))
}
const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false
@ -386,6 +426,7 @@ export const useChecklistBeforePublish = () => {
} = workflowStore.getState()
const nodes = getNodes()
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t)
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
if (maxDepth > MAX_TREE_DEPTH) {
@ -500,6 +541,12 @@ export const useChecklistBeforePublish = () => {
return false
}
const duplicateOutputMessages = duplicateEndOutputMessages.get(node!.id) || []
if (duplicateOutputMessages.length > 0) {
toast.error(`[${node!.data.title}] ${duplicateOutputMessages[0]}`)
return false
}
const availableVars = map[node!.id]!.availableVars
for (const variable of usedVars) {

View File

@ -27,6 +27,9 @@ describe('useWorkflowFinished', () => {
data: { status: 'succeeded', outputs: { a: 'hello', b: 'world' } },
} as WorkflowFinishedResponse)
expect(store.getState().workflowRunningData!.resultTabActive).toBeFalsy()
const state = store.getState().workflowRunningData!
expect(state.result.status).toBe('succeeded')
expect(state.resultTabActive).toBe(false)
expect(state.resultText).toBe('')
})
})

View File

@ -16,4 +16,52 @@ describe('useWorkflowTextChunk', () => {
expect(state.resultText).toBe('Hello World')
expect(state.resultTabActive).toBe(true)
})
it('inserts a line break when text chunks switch to a different output selector', () => {
const { result, store } = renderWorkflowHook(() => useWorkflowTextChunk(), {
initialStoreState: {
workflowRunningData: baseRunningData({ resultText: 'Hello', resultTextSelectorKey: 'end.answer' }),
},
})
result.current.handleWorkflowTextChunk({
data: { text: '42', from_variable_selector: ['end', 'count'] },
} as TextChunkResponse)
const state = store.getState().workflowRunningData!
expect(state.resultText).toBe('Hello\n42')
expect(state.resultTextSelectorKey).toBe('end.count')
})
it('does not add an extra line break when the incoming chunk already starts with one', () => {
const { result, store } = renderWorkflowHook(() => useWorkflowTextChunk(), {
initialStoreState: {
workflowRunningData: baseRunningData({ resultText: 'Hello', resultTextSelectorKey: 'end.answer' }),
},
})
result.current.handleWorkflowTextChunk({
data: { text: '\n42', from_variable_selector: ['end', 'count'] },
} as TextChunkResponse)
const state = store.getState().workflowRunningData!
expect(state.resultText).toBe('Hello\n42')
expect(state.resultTextSelectorKey).toBe('end.count')
})
it('does not insert a line break when text chunks stay on the same output selector', () => {
const { result, store } = renderWorkflowHook(() => useWorkflowTextChunk(), {
initialStoreState: {
workflowRunningData: baseRunningData({ resultText: 'Hello', resultTextSelectorKey: 'end.answer' }),
},
})
result.current.handleWorkflowTextChunk({
data: { text: ' world', from_variable_selector: ['end', 'answer'] },
} as TextChunkResponse)
const state = store.getState().workflowRunningData!
expect(state.resultText).toBe('Hello world')
expect(state.resultTextSelectorKey).toBe('end.answer')
})
})

View File

@ -40,6 +40,7 @@ export const useWorkflowStarted = () => {
status: WorkflowRunningStatus.Running,
}
draft.resultText = ''
draft.resultTextSelectorKey = undefined
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {

View File

@ -7,15 +7,26 @@ export const useWorkflowTextChunk = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowTextChunk = useCallback((params: TextChunkResponse) => {
const { data: { text } } = params
const { data: { text, from_variable_selector } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const nextSelectorKey = from_variable_selector?.join('.')
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultTabActive = true
const shouldInsertLineBreak = nextSelectorKey
&& draft.resultText
&& draft.resultTextSelectorKey
&& draft.resultTextSelectorKey !== nextSelectorKey
&& !draft.resultText.endsWith('\n')
&& !text.startsWith('\n')
if (shouldInsertLineBreak)
draft.resultText += '\n'
draft.resultText += text
if (nextSelectorKey)
draft.resultTextSelectorKey = nextSelectorKey
}))
}, [workflowStore])

View File

@ -14,6 +14,7 @@ export const useWorkflowTextReplace = () => {
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultText = text
draft.resultTextSelectorKey = undefined
}))
}, [workflowStore])

View File

@ -10,6 +10,7 @@ import type { FileUploadConfigResponse } from '@/models/common'
type PreviewRunningData = WorkflowRunningData & {
resultTabActive?: boolean
resultText?: string
resultTextSelectorKey?: string
// human input form schema or data cached when node is in 'Paused' status
extraContentAndFormData?: Record<string, unknown>
}

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "الرجاء إضافة عقدة البداية أولاً قبل {{operation}}",
"errorMsg.authRequired": "الترخيص مطلوب",
"errorMsg.configureModel": "قم بتكوين نموذج",
"errorMsg.duplicateOutputVariable": "متغير عقدة الإخراج \"{{variable}}\" مكرر. يجب أن تكون أسماء متغيرات عقدة الإخراج فريدة.",
"errorMsg.fieldRequired": "{{field}} مطلوب",
"errorMsg.fields.code": "الكود",
"errorMsg.fields.model": "النموذج",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Bitte füge zuerst einen Startknoten hinzu, bevor du {{operation}}.",
"errorMsg.authRequired": "Autorisierung ist erforderlich",
"errorMsg.configureModel": "Modell konfigurieren",
"errorMsg.duplicateOutputVariable": "Doppelte Variable des Output-Knotens \"{{variable}}\". Variablennamen von Output-Knoten müssen eindeutig sein.",
"errorMsg.fieldRequired": "{{field}} ist erforderlich",
"errorMsg.fields.code": "Code",
"errorMsg.fields.model": "Modell",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Please add a start node first before {{operation}}",
"errorMsg.authRequired": "Authorization is required",
"errorMsg.configureModel": "Configure a model",
"errorMsg.duplicateOutputVariable": "Duplicate Output node variable \"{{variable}}\". Output node variable names must be unique.",
"errorMsg.fieldRequired": "{{field}} is required",
"errorMsg.fields.code": "Code",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Por favor, agregue primero un nodo de inicio antes de {{operation}}",
"errorMsg.authRequired": "Se requiere autorización",
"errorMsg.configureModel": "Configura un modelo",
"errorMsg.duplicateOutputVariable": "Variable duplicada del nodo de salida \"{{variable}}\". Los nombres de las variables del nodo de salida deben ser únicos.",
"errorMsg.fieldRequired": "Se requiere {{field}}",
"errorMsg.fields.code": "Código",
"errorMsg.fields.model": "Modelo",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "لطفاً قبل از {{operation}} ابتدا یک گره شروع اضافه کنید",
"errorMsg.authRequired": "احراز هویت الزامی است",
"errorMsg.configureModel": "یک مدل پیکربندی کنید",
"errorMsg.duplicateOutputVariable": "متغیر گره خروجی «{{variable}}» تکراری است. نام متغیرهای گره خروجی باید یکتا باشند.",
"errorMsg.fieldRequired": "{{field}} الزامی است",
"errorMsg.fields.code": "کد",
"errorMsg.fields.model": "مدل",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Veuillez d'abord ajouter un nœud de départ avant {{operation}}",
"errorMsg.authRequired": "Autorisation requise",
"errorMsg.configureModel": "Configurez un modèle",
"errorMsg.duplicateOutputVariable": "Variable du nœud de sortie en double \"{{variable}}\". Les noms des variables du nœud de sortie doivent être uniques.",
"errorMsg.fieldRequired": "{{field}} est requis",
"errorMsg.fields.code": "Code",
"errorMsg.fields.model": "Modèle",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "कृपया {{operation}} से पहले एक प्रारंभ नोड जोड़ें",
"errorMsg.authRequired": "प्राधिकरण आवश्यक है",
"errorMsg.configureModel": "एक मॉडल कॉन्फ़िगर करें",
"errorMsg.duplicateOutputVariable": "आउटपुट नोड वेरिएबल \"{{variable}}\" डुप्लिकेट है। आउटपुट नोड वेरिएबल के नाम यूनिक होने चाहिए।",
"errorMsg.fieldRequired": "{{field}} आवश्यक है",
"errorMsg.fields.code": "कोड",
"errorMsg.fields.model": "मॉडल",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Silakan tambahkan node awal terlebih dahulu sebelum {{operation}}",
"errorMsg.authRequired": "Otorisasi diperlukan",
"errorMsg.configureModel": "Konfigurasikan model",
"errorMsg.duplicateOutputVariable": "Variabel node output duplikat \"{{variable}}\". Nama variabel node output harus unik.",
"errorMsg.fieldRequired": "{{field}} wajib diisi",
"errorMsg.fields.code": "Kode",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Per favore aggiungi prima un nodo iniziale prima di {{operation}}",
"errorMsg.authRequired": "È richiesta l'autorizzazione",
"errorMsg.configureModel": "Configura un modello",
"errorMsg.duplicateOutputVariable": "Variabile del nodo di output duplicata \"{{variable}}\". I nomi delle variabili del nodo di output devono essere univoci.",
"errorMsg.fieldRequired": "{{field}} è richiesto",
"errorMsg.fields.code": "Codice",
"errorMsg.fields.model": "Modello",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "{{operation}}前に開始ノードを追加してください",
"errorMsg.authRequired": "認証が必要です",
"errorMsg.configureModel": "モデルを設定してください",
"errorMsg.duplicateOutputVariable": "出力ノード変数「{{variable}}」が重複しています。出力ノード変数名は一意である必要があります。",
"errorMsg.fieldRequired": "{{field}} は必須です",
"errorMsg.fields.code": "コード",
"errorMsg.fields.model": "モデル",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "{{operation}} 전에 먼저 시작 노드를 추가해 주세요",
"errorMsg.authRequired": "인증이 필요합니다",
"errorMsg.configureModel": "모델을 구성하세요",
"errorMsg.duplicateOutputVariable": "출력 노드 변수 \"{{variable}}\"가 중복되었습니다. 출력 노드 변수 이름은 고유해야 합니다.",
"errorMsg.fieldRequired": "{{field}}가 필요합니다",
"errorMsg.fields.code": "코드",
"errorMsg.fields.model": "모델",

View File

@ -130,7 +130,7 @@
"comments.filter.all": "Alles",
"comments.filter.onlyYourThreads": "Alleen jouw threads",
"comments.filter.showResolved": "Opgeloste tonen",
"comments.loading": "Laden\u2026",
"comments.loading": "Laden",
"comments.noComments": "Nog geen reacties",
"comments.panelTitle": "Reactie",
"comments.placeholder.add": "Voeg een reactie toe",
@ -344,6 +344,7 @@
"error.startNodeRequired": "Please add a start node first before {{operation}}",
"errorMsg.authRequired": "Authorization is required",
"errorMsg.configureModel": "Configureer een model",
"errorMsg.duplicateOutputVariable": "Dubbele variabele van de outputnode \"{{variable}}\". Namen van outputnode-variabelen moeten uniek zijn.",
"errorMsg.fieldRequired": "{{field}} is required",
"errorMsg.fields.code": "Code",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Najpierw dodaj węzeł początkowy przed {{operation}}",
"errorMsg.authRequired": "Wymagana autoryzacja",
"errorMsg.configureModel": "Skonfiguruj model",
"errorMsg.duplicateOutputVariable": "Zduplikowana zmienna węzła wyjściowego \"{{variable}}\". Nazwy zmiennych węzła wyjściowego muszą być unikalne.",
"errorMsg.fieldRequired": "{{field}} jest wymagane",
"errorMsg.fields.code": "Kod",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Por favor, adicione um nó inicial antes de {{operation}}",
"errorMsg.authRequired": "Autorização é necessária",
"errorMsg.configureModel": "Configure um modelo",
"errorMsg.duplicateOutputVariable": "Variável duplicada do nó de saída \"{{variable}}\". Os nomes das variáveis do nó de saída devem ser únicos.",
"errorMsg.fieldRequired": "{{field}} é obrigatório",
"errorMsg.fields.code": "Código",
"errorMsg.fields.model": "Modelo",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Vă rugăm să adăugați mai întâi un nod de pornire înainte de {{operation}}",
"errorMsg.authRequired": "Autorizarea este necesară",
"errorMsg.configureModel": "Configurați un model",
"errorMsg.duplicateOutputVariable": "Variabilă duplicată a nodului de ieșire „{{variable}}”. Numele variabilelor nodului de ieșire trebuie să fie unice.",
"errorMsg.fieldRequired": "{{field}} este obligatoriu",
"errorMsg.fields.code": "Cod",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Пожалуйста, сначала добавьте начальный узел перед {{operation}}",
"errorMsg.authRequired": "Требуется авторизация",
"errorMsg.configureModel": "Настройте модель",
"errorMsg.duplicateOutputVariable": "Повторяющаяся переменная узла вывода \"{{variable}}\". Имена переменных узла вывода должны быть уникальными.",
"errorMsg.fieldRequired": "{{field}} обязательно для заполнения",
"errorMsg.fields.code": "Код",
"errorMsg.fields.model": "Модель",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Prosimo, najprej dodajte začetni vozel pred {{operation}}",
"errorMsg.authRequired": "Zahtevana je avtorizacija",
"errorMsg.configureModel": "Konfiguriraj model",
"errorMsg.duplicateOutputVariable": "Podvojena spremenljivka izhodnega vozlišča \"{{variable}}\". Imena spremenljivk izhodnega vozlišča morajo biti enolična.",
"errorMsg.fieldRequired": "{{field}} je obvezno",
"errorMsg.fields.code": "Koda",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "โปรดเพิ่มโหนดเริ่มต้นก่อน {{operation}}",
"errorMsg.authRequired": "ต้องได้รับอนุญาต",
"errorMsg.configureModel": "กำหนดค่าโมเดล",
"errorMsg.duplicateOutputVariable": "ตัวแปรของโหนดเอาต์พุต \"{{variable}}\" ซ้ำกัน ชื่อตัวแปรของโหนดเอาต์พุตต้องไม่ซ้ำกัน",
"errorMsg.fieldRequired": "{{field}} เป็นสิ่งจําเป็น",
"errorMsg.fields.code": "รหัส",
"errorMsg.fields.model": "แบบ",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Lütfen {{operation}} işleminden önce önce bir başlangıç düğümü ekleyin",
"errorMsg.authRequired": "Yetkilendirme gereklidir",
"errorMsg.configureModel": "Bir model yapılandırın",
"errorMsg.duplicateOutputVariable": "Yinelenen çıktı düğümü değişkeni \"{{variable}}\". Çıktı düğümü değişken adları benzersiz olmalıdır.",
"errorMsg.fieldRequired": "{{field}} gereklidir",
"errorMsg.fields.code": "Kod",
"errorMsg.fields.model": "Model",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Будь ласка, спершу додайте стартовий вузол перед {{operation}}",
"errorMsg.authRequired": "Потрібна авторизація",
"errorMsg.configureModel": "Налаштуйте модель",
"errorMsg.duplicateOutputVariable": "Дубльована змінна вузла виводу \"{{variable}}\". Назви змінних вузла виводу мають бути унікальними.",
"errorMsg.fieldRequired": "{{field}} є обов'язковим",
"errorMsg.fields.code": "Код",
"errorMsg.fields.model": "Модель",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "Vui lòng thêm một nút bắt đầu trước {{operation}}",
"errorMsg.authRequired": "Yêu cầu xác thực",
"errorMsg.configureModel": "Cấu hình mô hình",
"errorMsg.duplicateOutputVariable": "Biến của nút đầu ra \"{{variable}}\" bị trùng lặp. Tên biến của nút đầu ra phải là duy nhất.",
"errorMsg.fieldRequired": "{{field}} là bắt buộc",
"errorMsg.fields.code": "Mã",
"errorMsg.fields.model": "Mô hình",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "请先添加开始节点,然后再{{operation}}",
"errorMsg.authRequired": "请先授权",
"errorMsg.configureModel": "请配置模型",
"errorMsg.duplicateOutputVariable": "输出节点变量“{{variable}}”重复。输出节点变量名必须唯一。",
"errorMsg.fieldRequired": "{{field}} 不能为空",
"errorMsg.fields.code": "代码",
"errorMsg.fields.model": "模型",

View File

@ -344,6 +344,7 @@
"error.startNodeRequired": "請先新增一個起始節點,再執行 {{operation}}",
"errorMsg.authRequired": "請先授權",
"errorMsg.configureModel": "請配置模型",
"errorMsg.duplicateOutputVariable": "輸出節點變數「{{variable}}」重複。輸出節點變數名稱必須唯一。",
"errorMsg.fieldRequired": "{{field}} 不能為空",
"errorMsg.fields.code": "程式碼",
"errorMsg.fields.model": "模型",

View File

@ -305,6 +305,7 @@ export type TextChunkResponse = {
event: string
data: {
text: string
from_variable_selector?: string[]
}
}