fix(editor): "Trigger node not found" error when chat message is entered (#14954)

This commit is contained in:
Suguru Inoue
2025-04-29 13:45:30 +02:00
committed by GitHub
parent 7f89244304
commit 8981e22dd4
9 changed files with 125 additions and 107 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watchEffect, useTemplateRef } from 'vue'; import { computed, ref, watchEffect, useTemplateRef, watch } from 'vue';
// Components // Components
import ChatMessagesPanel from './components/ChatMessagesPanel.vue'; import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
@@ -68,7 +68,7 @@ const {
sendMessage, sendMessage,
refreshSession, refreshSession,
displayExecution, displayExecution,
} = useChatState(false, onWindowResize); } = useChatState(false);
// Expose internal state for testing // Expose internal state for testing
defineExpose({ defineExpose({
@@ -91,6 +91,18 @@ function onPopOut() {
watchEffect(() => { watchEffect(() => {
canvasStore.setPanelHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0); canvasStore.setPanelHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0);
}); });
watch(
() => workflowsStore.logsPanelState,
(state) => {
if (state !== LOGS_PANEL_STATE.CLOSED) {
setTimeout(() => {
onWindowResize?.();
}, 0);
}
},
{ immediate: true },
);
</script> </script>
<template> <template>

View File

@@ -7,7 +7,7 @@ import MessageOptionAction from './MessageOptionAction.vue';
import { chatEventBus } from '@n8n/chat/event-buses'; import { chatEventBus } from '@n8n/chat/event-buses';
import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue'; import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
import ChatInput from '@n8n/chat/components/Input.vue'; import ChatInput from '@n8n/chat/components/Input.vue';
import { computed, ref } from 'vue'; import { watch, computed, ref } from 'vue';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue'; import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue';
@@ -136,6 +136,18 @@ async function copySessionId() {
type: 'success', type: 'success',
}); });
} }
watch(
() => props.isOpen,
(isOpen) => {
if (isOpen) {
setTimeout(() => {
chatEventBus.emit('focusInput');
}, 0);
}
},
{ immediate: true },
);
</script> </script>
<template> <template>

View File

@@ -6,7 +6,6 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow'; import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { type INodeUi } from '@/Interface'; import { type INodeUi } from '@/Interface';
import { useCanvasStore } from '@/stores/canvas.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants'; import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
@@ -31,11 +30,10 @@ interface ChatState {
displayExecution: (executionId: string) => void; displayExecution: (executionId: string) => void;
} }
export function useChatState(isReadOnly: boolean, onWindowResize?: () => void): ChatState { export function useChatState(isReadOnly: boolean): ChatState {
const locale = useI18n(); const locale = useI18n();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const canvasStore = useCanvasStore();
const router = useRouter(); const router = useRouter();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
@@ -44,25 +42,16 @@ export function useChatState(isReadOnly: boolean, onWindowResize?: () => void):
const currentSessionId = ref<string>(uuid().replace(/-/g, '')); const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages); const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
const canvasNodes = computed(() => workflowsStore.allNodes);
const allConnections = computed(() => workflowsStore.allConnections);
const logsPanelState = computed(() => workflowsStore.logsPanelState); const logsPanelState = computed(() => workflowsStore.logsPanelState);
const workflow = computed(() => workflowsStore.getCurrentWorkflow()); const workflow = computed(() => workflowsStore.getCurrentWorkflow());
// Initialize features with injected dependencies // Initialize features with injected dependencies
const { const { chatTriggerNode, connectedNode, allowFileUploads, allowedFilesMimeTypes } =
chatTriggerNode, useChatTrigger({
connectedNode, workflow,
allowFileUploads, getNodeByName: workflowsStore.getNodeByName,
allowedFilesMimeTypes, getNodeType: nodeTypesStore.getNodeType,
setChatTriggerNode, });
setConnectedNode,
} = useChatTrigger({
workflow,
canvasNodes,
getNodeByName: workflowsStore.getNodeByName,
getNodeType: nodeTypesStore.getNodeType,
});
const { sendMessage, isLoading } = useChatMessaging({ const { sendMessage, isLoading } = useChatMessaging({
chatTrigger: chatTriggerNode, chatTrigger: chatTriggerNode,
@@ -134,37 +123,6 @@ export function useChatState(isReadOnly: boolean, onWindowResize?: () => void):
provide(ChatSymbol, chatConfig); provide(ChatSymbol, chatConfig);
provide(ChatOptionsSymbol, chatOptions); provide(ChatOptionsSymbol, chatOptions);
// Watchers
watch(
() => logsPanelState.value,
(state) => {
if (state !== LOGS_PANEL_STATE.CLOSED) {
setChatTriggerNode();
setConnectedNode();
setTimeout(() => {
onWindowResize?.();
chatEventBus.emit('focusInput');
}, 0);
}
},
{ immediate: true },
);
watch(
() => allConnections.value,
() => {
if (canvasStore.isLoading) return;
setTimeout(() => {
if (!chatTriggerNode.value) {
setChatTriggerNode();
}
setConnectedNode();
}, 0);
},
{ deep: true },
);
// This function creates a promise that resolves when the workflow execution completes // This function creates a promise that resolves when the workflow execution completes
// It's used to handle the loading state while waiting for the workflow to finish // It's used to handle the loading state while waiting for the workflow to finish
async function createExecutionPromise() { async function createExecutionPromise() {

View File

@@ -1,11 +1,11 @@
import type { ComputedRef, MaybeRef } from 'vue'; import type { ComputedRef } from 'vue';
import { ref, computed, unref } from 'vue'; import { computed } from 'vue';
import { import {
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE, CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
NodeConnectionTypes, NodeConnectionTypes,
NodeHelpers, NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { INodeTypeDescription, Workflow, INode, INodeParameters } from 'n8n-workflow'; import type { INodeTypeDescription, Workflow, INodeParameters } from 'n8n-workflow';
import { import {
AI_CATEGORY_AGENTS, AI_CATEGORY_AGENTS,
AI_CATEGORY_CHAINS, AI_CATEGORY_CHAINS,
@@ -18,21 +18,12 @@ import { isChatNode } from '@/components/CanvasChat/utils';
export interface ChatTriggerDependencies { export interface ChatTriggerDependencies {
getNodeByName: (name: string) => INodeUi | null; getNodeByName: (name: string) => INodeUi | null;
getNodeType: (type: string, version: number) => INodeTypeDescription | null; getNodeType: (type: string, version: number) => INodeTypeDescription | null;
canvasNodes: MaybeRef<INodeUi[]>;
workflow: ComputedRef<Workflow>; workflow: ComputedRef<Workflow>;
} }
export function useChatTrigger({ export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
getNodeByName, const chatTriggerNode = computed(
getNodeType, () => Object.values(workflow.value.nodes).find(isChatNode) ?? null,
canvasNodes,
workflow,
}: ChatTriggerDependencies) {
const chatTriggerName = ref<string | null>(null);
const connectedNode = ref<INode | null>(null);
const chatTriggerNode = computed(() =>
chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null,
); );
const allowFileUploads = computed(() => { const allowFileUploads = computed(() => {
@@ -49,22 +40,12 @@ export function useChatTrigger({
); );
}); });
/** Gets the chat trigger node from the workflow */
function setChatTriggerNode() {
const triggerNode = unref(canvasNodes).find(isChatNode);
if (!triggerNode) {
return;
}
chatTriggerName.value = triggerNode.name;
}
/** Sets the connected node after finding the trigger */ /** Sets the connected node after finding the trigger */
function setConnectedNode() { const connectedNode = computed(() => {
const triggerNode = chatTriggerNode.value; const triggerNode = chatTriggerNode.value;
if (!triggerNode) { if (!triggerNode) {
return; return null;
} }
const chatChildren = workflow.value.getChildNodes(triggerNode.name); const chatChildren = workflow.value.getChildNodes(triggerNode.name);
@@ -121,15 +102,14 @@ export function useChatTrigger({
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent)); const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
return result; return result;
}); });
connectedNode.value = chatRootNode ?? null;
} return chatRootNode ?? null;
});
return { return {
allowFileUploads, allowFileUploads,
allowedFilesMimeTypes, allowedFilesMimeTypes,
chatTriggerNode, chatTriggerNode,
connectedNode: computed(() => connectedNode.value), connectedNode,
setChatTriggerNode,
setConnectedNode,
}; };
} }

View File

@@ -146,6 +146,17 @@ function isLastChild(level: number) {
icon="exclamation-triangle" icon="exclamation-triangle"
:class="$style.compactErrorIcon" :class="$style.compactErrorIcon"
/> />
<N8nIconButton
v-if="!isCompact || !props.latestInfo?.deleted"
type="secondary"
size="medium"
icon="edit"
style="color: var(--color-text-base)"
:disabled="props.latestInfo?.deleted"
:class="$style.openNdvButton"
:aria-label="locale.baseText('logs.overview.body.open')"
@click.stop="emit('openNdv', props.data)"
/>
<N8nIconButton <N8nIconButton
v-if=" v-if="
!isCompact || !isCompact ||
@@ -160,17 +171,6 @@ function isLastChild(level: number) {
:disabled="props.latestInfo?.deleted || props.latestInfo?.disabled" :disabled="props.latestInfo?.deleted || props.latestInfo?.disabled"
@click.stop="emit('triggerPartialExecution', props.data)" @click.stop="emit('triggerPartialExecution', props.data)"
/> />
<N8nIconButton
v-if="!isCompact || !props.latestInfo?.deleted"
type="secondary"
size="small"
icon="external-link-alt"
style="color: var(--color-text-base)"
:disabled="props.latestInfo?.deleted"
:class="$style.openNdvButton"
:aria-label="locale.baseText('logs.overview.body.open')"
@click.stop="emit('openNdv', props.data)"
/>
<N8nButton <N8nButton
v-if="!isCompact || props.data.children.length > 0" v-if="!isCompact || props.data.children.length > 0"
type="secondary" type="secondary"

View File

@@ -9,6 +9,7 @@ import {
import { import {
createAiData, createAiData,
createLogEntries, createLogEntries,
deepToRaw,
findSelectedLogEntry, findSelectedLogEntry,
getTreeNodeData, getTreeNodeData,
getTreeNodeDataV2, getTreeNodeDataV2,
@@ -21,6 +22,7 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { type LogEntrySelection } from '../CanvasChat/types/logs'; import { type LogEntrySelection } from '../CanvasChat/types/logs';
import { type IExecutionResponse } from '@/Interface'; import { type IExecutionResponse } from '@/Interface';
import { isReactive, reactive } from 'vue';
describe(getTreeNodeData, () => { describe(getTreeNodeData, () => {
it('should generate one node per execution', () => { it('should generate one node per execution', () => {
@@ -608,3 +610,28 @@ describe(createLogEntries, () => {
]); ]);
}); });
}); });
describe(deepToRaw, () => {
it('should convert reactive fields to raw in data with circular structure', () => {
const data = reactive({
foo: reactive({ bar: {} }),
bazz: {},
});
data.foo.bar = data;
data.bazz = data;
const raw = deepToRaw(data);
expect(isReactive(data)).toBe(true);
expect(isReactive(data.foo)).toBe(true);
expect(isReactive(data.foo.bar)).toBe(true);
expect(isReactive(data.bazz)).toBe(true);
expect(isReactive(raw)).toBe(false);
expect(isReactive(raw.foo)).toBe(false);
expect(isReactive(raw.foo.bar)).toBe(false);
expect(isReactive(raw.bazz)).toBe(false);
console.log(raw.foo.bar);
});
});

View File

@@ -431,8 +431,18 @@ export function findSelectedLogEntry(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function deepToRaw<T>(sourceObj: T): T { export function deepToRaw<T>(sourceObj: T): T {
const seen = new WeakMap();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const objectIterator = (input: any): any => { const objectIterator = (input: any): any => {
if (seen.has(input)) {
return input;
}
if (input !== null && typeof input === 'object') {
seen.set(input, true);
}
if (Array.isArray(input)) { if (Array.isArray(input)) {
return input.map((item) => objectIterator(item)); return input.map((item) => objectIterator(item));
} }

View File

@@ -505,6 +505,13 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
} }
workflowsStore.executingNode.length = 0; workflowsStore.executingNode.length = 0;
if (receivedData.type === 'executionFinished') {
// As a temporary workaround for https://linear.app/n8n/issue/PAY-2762,
// remove runs that is still 'running' status when execution is finished
executionData = removeRunningTaskData(executionData as IExecutionResponse);
}
workflowsStore.setWorkflowExecutionData(executionData as IExecutionResponse); workflowsStore.setWorkflowExecutionData(executionData as IExecutionResponse);
uiStore.removeActiveAction('workflowRunning'); uiStore.removeActiveAction('workflowRunning');
@@ -647,3 +654,27 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
retryTimeout, retryTimeout,
}; };
} }
function removeRunningTaskData(execution: IExecutionResponse): IExecutionResponse {
if (!execution.data) {
return execution;
}
return {
...execution,
data: {
...execution.data,
resultData: {
...execution.data.resultData,
runData: Object.fromEntries(
Object.entries(execution.data.resultData.runData)
.map(([nodeName, runs]) => [
nodeName,
runs.filter((run) => run.executionStatus !== 'running'),
])
.filter(([, runs]) => runs.length > 0),
),
},
},
};
}

View File

@@ -157,18 +157,6 @@ export const faPopOut: IconDefinition = {
], ],
}; };
export const faTable: IconDefinition = {
prefix: 'fas',
iconName: 'table' as IconName,
icon: [
12,
12,
[],
'',
'M10.875 0C11.4844 0 12 0.589286 12 1.28571V10.7143C12 11.4375 11.4844 12 10.875 12H1.125C0.492188 12 0 11.4375 0 10.7143V1.28571C0 0.589286 0.492188 0 1.125 0H10.875ZM5.25 10.2857V7.71429H1.5V10.2857H5.25ZM5.25 6V3.42857H1.5V6H5.25ZM10.5 10.2857V7.71429H6.75V10.2857H10.5ZM10.5 6V3.42857H6.75V6H10.5Z',
],
};
export const faSchema: IconDefinition = { export const faSchema: IconDefinition = {
prefix: 'fas', prefix: 'fas',
iconName: 'schema' as IconName, iconName: 'schema' as IconName,