chore: try to track why nodes missing

This commit is contained in:
hjlarry 2026-04-10 11:04:03 +08:00
parent 692bf7ced1
commit a68482da77
5 changed files with 132 additions and 15 deletions

View File

@ -42,10 +42,17 @@ const Header = () => {
else
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
const handleDownloadGraphImportLog = useCallback(() => {
void import('@/app/components/workflow/collaboration/core/collaboration-manager')
.then(({ collaborationManager }) => {
collaborationManager.downloadGraphImportLog()
})
.catch(() => {})
}, [])
const renderLogo = () => (
<h1>
<Link href="/apps" className="flex h-8 shrink-0 items-center justify-center overflow-hidden whitespace-nowrap px-0.5 indent-[-9999px]">
<Link href="/apps" className="flex h-8 shrink-0 items-center justify-center overflow-hidden px-0.5 indent-[-9999px] whitespace-nowrap">
{isBrandingEnabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
@ -91,7 +98,7 @@ const Header = () => {
return (
<div className="flex h-[56px] items-center">
<div className="flex min-w-0 flex-1 items-center pl-3 pr-2 min-[1280px]:pr-3">
<div className="flex min-w-0 flex-1 items-center pr-2 pl-3 min-[1280px]:pr-3">
{renderLogo()}
<div className="mx-1.5 shrink-0 font-light text-divider-deep">/</div>
<WorkspaceProvider>
@ -105,7 +112,15 @@ const Header = () => {
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
<div className="flex min-w-0 flex-1 items-center justify-end pl-2 pr-3 min-[1280px]:pl-3">
<div className="flex min-w-0 flex-1 items-center justify-end pr-3 pl-2 min-[1280px]:pl-3">
<button
type="button"
data-testid="workflow-import-log-download"
className="top-1/2 left-full ml-1 h-3 w-3 -translate-y-1/2 opacity-0"
aria-hidden="true"
tabIndex={-1}
onClick={handleDownloadGraphImportLog}
/>
<EnvNav />
<div className="mr-2">
<PluginsNav />

View File

@ -78,7 +78,32 @@ type GraphImportLogEntry = {
}
}
type SetNodesAnomalyReason = 'node_count_decrease' | 'start_removed'
type SetNodesAnomalyLogEntry = {
timestamp: number
appId: string | null
source: string
reasons: SetNodesAnomalyReason[]
oldCount: number
newCount: number
removedNodeIds: string[]
oldStartNodeIds: string[]
newStartNodeIds: string[]
oldNodeIds: string[]
newNodeIds: string[]
visibilityState: DocumentVisibilityState | 'unknown'
meta: {
leaderId: string | null
isLeader: boolean
graphViewActive: boolean | null
pendingInitialSync: boolean
isConnected: boolean
}
}
const GRAPH_IMPORT_LOG_LIMIT = 20
const SET_NODES_ANOMALY_LOG_LIMIT = 100
const toLoroValue = (value: unknown): Value => cloneDeep(value) as Value
const toLoroRecord = (value: unknown): Record<string, Value> => cloneDeep(value) as Record<string, Value>
@ -102,6 +127,7 @@ export class CollaborationManager {
private pendingGraphImportEmit = false
private graphViewActive: boolean | null = null
private graphImportLogs: GraphImportLogEntry[] = []
private setNodesAnomalyLogs: SetNodesAnomalyLogEntry[] = []
private pendingImportLog: {
timestamp: number
sources: Set<'nodes' | 'edges'>
@ -391,7 +417,7 @@ export class CollaborationManager {
this.connect(appId, reactFlowStore)
}
setNodes = (oldNodes: Node[], newNodes: Node[]): void => {
setNodes = (oldNodes: Node[], newNodes: Node[], source = 'collaboration-manager:setNodes'): void => {
if (!this.doc)
return
@ -399,6 +425,7 @@ export class CollaborationManager {
if (this.isUndoRedoInProgress)
return
this.captureSetNodesAnomaly(oldNodes, newNodes, source)
this.syncNodes(oldNodes, newNodes)
this.doc.commit()
}
@ -480,8 +507,9 @@ export class CollaborationManager {
if (value?.value && typeof value.value === 'object' && 'selectedNodeId' in value.value && this.reactFlowStore) {
const selectedNodeId = (value.value as { selectedNodeId?: string | null }).selectedNodeId
if (selectedNodeId) {
const { setNodes } = this.reactFlowStore.getState()
const nodes = this.reactFlowStore.getState().getNodes()
const state = this.reactFlowStore.getState()
const { setNodes } = state
const nodes = state.getNodes()
const newNodes = nodes.map((n: Node) => ({
...n,
data: {
@ -489,6 +517,7 @@ export class CollaborationManager {
selected: n.id === selectedNodeId,
},
}))
this.captureSetNodesAnomaly(nodes, newNodes, 'reactflow-native:undo-redo-selection-restore')
setNodes(newNodes)
}
}
@ -791,9 +820,11 @@ export class CollaborationManager {
requestAnimationFrame(() => {
// Get ReactFlow's native setters, not the collaborative ones
const state = reactFlowStore.getState()
const previousNodes = state.getNodes()
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
// Call ReactFlow's native setters directly to avoid triggering collaboration
this.captureSetNodesAnomaly(previousNodes, updatedNodes, 'reactflow-native:undo-apply')
state.setNodes(updatedNodes)
state.setEdges(updatedEdges)
@ -831,9 +862,11 @@ export class CollaborationManager {
requestAnimationFrame(() => {
// Get ReactFlow's native setters, not the collaborative ones
const state = reactFlowStore.getState()
const previousNodes = state.getNodes()
const updatedNodes = Array.from(this.nodesMap?.values() || []) as Node[]
const updatedEdges = Array.from(this.edgesMap?.values() || []) as Edge[]
// Call ReactFlow's native setters directly to avoid triggering collaboration
this.captureSetNodesAnomaly(previousNodes, updatedNodes, 'reactflow-native:redo-apply')
state.setNodes(updatedNodes)
state.setEdges(updatedEdges)
@ -972,6 +1005,7 @@ export class CollaborationManager {
})
// Call ReactFlow's native setter directly to avoid triggering collaboration
this.captureSetNodesAnomaly(previousNodes, updatedNodes, 'reactflow-native:import-nodes-map-subscribe')
state.setNodes(updatedNodes)
this.scheduleGraphImportEmit()
@ -1067,17 +1101,37 @@ export class CollaborationManager {
clearGraphImportLog(): void {
this.graphImportLogs = []
this.setNodesAnomalyLogs = []
this.pendingImportLog = null
}
downloadGraphImportLog(): void {
if (this.graphImportLogs.length === 0)
return
const reactFlowState = this.reactFlowStore?.getState()
const payload = {
appId: this.currentAppId,
generatedAt: new Date().toISOString(),
entries: this.graphImportLogs,
setNodesAnomalies: this.setNodesAnomalyLogs,
summary: {
logCount: this.graphImportLogs.length,
setNodesAnomalyCount: this.setNodesAnomalyLogs.length,
leaderId: this.leaderId,
isLeader: this.isLeader,
graphViewActive: this.graphViewActive,
pendingInitialSync: this.pendingInitialSync,
isConnected: this.isConnected(),
hasDoc: Boolean(this.doc),
hasReactFlowStore: Boolean(this.reactFlowStore),
onlineUsersCount: this.onlineUsers.length,
crdtCounts: {
nodes: this.getNodes().length,
edges: this.getEdges().length,
},
reactFlowCounts: {
nodes: reactFlowState?.getNodes().length ?? 0,
edges: reactFlowState?.getEdges().length ?? 0,
},
},
}
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
const appSuffix = this.currentAppId ?? 'unknown'
@ -1091,6 +1145,54 @@ export class CollaborationManager {
URL.revokeObjectURL(url)
}
private captureSetNodesAnomaly(oldNodes: Node[], newNodes: Node[], source: string): void {
const oldNodeIds = oldNodes.map(node => node.id)
const newNodeIds = newNodes.map(node => node.id)
const newNodeIdSet = new Set(newNodeIds)
const removedNodeIds = oldNodeIds.filter(nodeId => !newNodeIdSet.has(nodeId))
const oldStartNodeIds = oldNodes
.filter(node => (node.data as CommonNodeType | undefined)?.type === 'start')
.map(node => node.id)
const newStartNodeIds = newNodes
.filter(node => (node.data as CommonNodeType | undefined)?.type === 'start')
.map(node => node.id)
const reasons: SetNodesAnomalyReason[] = []
if (newNodes.length < oldNodes.length)
reasons.push('node_count_decrease')
if (oldStartNodeIds.length > 0 && newStartNodeIds.length === 0)
reasons.push('start_removed')
if (!reasons.length)
return
const entry: SetNodesAnomalyLogEntry = {
timestamp: Date.now(),
appId: this.currentAppId,
source,
reasons,
oldCount: oldNodes.length,
newCount: newNodes.length,
removedNodeIds,
oldStartNodeIds,
newStartNodeIds,
oldNodeIds,
newNodeIds,
visibilityState: typeof document === 'undefined' ? 'unknown' : document.visibilityState,
meta: {
leaderId: this.leaderId,
isLeader: this.isLeader,
graphViewActive: this.graphViewActive,
pendingInitialSync: this.pendingInitialSync,
isConnected: this.isConnected(),
},
}
this.setNodesAnomalyLogs.push(entry)
if (this.setNodesAnomalyLogs.length > SET_NODES_ANOMALY_LOG_LIMIT)
this.setNodesAnomalyLogs.splice(0, this.setNodesAnomalyLogs.length - SET_NODES_ANOMALY_LOG_LIMIT)
}
private snapshotReactFlowGraph(): { nodes: Node[], edges: Edge[] } {
if (!this.reactFlowStore) {
return {

View File

@ -39,13 +39,14 @@ export const useCollaborativeWorkflow = () => {
const store = useStoreApi()
const { setNodes: collabSetNodes, setEdges: collabSetEdges } = collaborationManager
const setNodes = useCallback((newNodes: Node[], shouldBroadcast: boolean = true) => {
const setNodes = useCallback((newNodes: Node[], shouldBroadcast: boolean = true, source = 'use-collaborative-workflow:setNodes') => {
const { getNodes, setNodes: reactFlowSetNodes } = store.getState()
if (shouldBroadcast) {
const oldNodes = getNodes()
collabSetNodes(
oldNodes.map(sanitizeNodeForBroadcast),
newNodes.map(sanitizeNodeForBroadcast),
source,
)
}
reactFlowSetNodes(newNodes)

View File

@ -45,7 +45,7 @@ export const usePerformRestore = () => {
const currentNodes = collaborationManager.getNodes()
const currentEdges = collaborationManager.getEdges()
collaborationManager.setNodes(currentNodes, nodes)
collaborationManager.setNodes(currentNodes, nodes, 'leader-restore:apply-graph')
collaborationManager.setEdges(currentEdges, edges)
collaborationManager.refreshGraphSynchronously()

View File

@ -627,7 +627,6 @@ export const useNodesInteractions = () => {
)
const { deleteNodeInspectorVars } = useInspectVarsCrud()
const handleNodeDelete = useCallback(
(nodeId: string) => {
if (getNodesReadOnly())
@ -766,7 +765,7 @@ export const useNodesInteractions = () => {
})
draft.splice(currentNodeIndex, 1)
})
setNodes(newNodes)
setNodes(newNodes, true, 'nodes:perform-batch-cascade-delete')
const newEdges = produce(edges, (draft) => {
return draft.filter(
edge =>
@ -2116,7 +2115,7 @@ export const useNodesInteractions = () => {
const shouldBroadcast = collaborationManager.isConnected()
setEdges(edges, shouldBroadcast)
setNodes(nodes, shouldBroadcast)
setNodes(nodes, shouldBroadcast, 'nodes:history-back')
if (shouldBroadcast)
collaborationManager.emitHistoryAction('undo')
workflowStore.setState({ edgeMenu: undefined })
@ -2141,7 +2140,7 @@ export const useNodesInteractions = () => {
const shouldBroadcast = collaborationManager.isConnected()
setEdges(edges, shouldBroadcast)
setNodes(nodes, shouldBroadcast)
setNodes(nodes, shouldBroadcast, 'nodes:history-forward')
if (shouldBroadcast)
collaborationManager.emitHistoryAction('redo')
workflowStore.setState({ edgeMenu: undefined })