feat: Add @agent icon and implement agent alias variables in workflow

inspector
This commit is contained in:
zhsama 2026-01-27 02:41:57 +08:00
parent 772dbe620d
commit 54fce5e903
16 changed files with 492 additions and 110 deletions

View File

@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M11.6665 7.00008C11.6665 4.42275 9.57718 2.33341 6.99984 2.33341C4.42251 2.33341 2.33317 4.42275 2.33317 7.00008C2.33317 9.57742 4.42251 11.6667 6.99984 11.6667C7.95755 11.6667 8.8479 11.3782 9.58873 10.8834L10.2359 11.8542C9.30995 12.4728 8.19701 12.8334 6.99984 12.8334C3.77817 12.8334 1.1665 10.2217 1.1665 7.00008C1.1665 3.77842 3.77817 1.16675 6.99984 1.16675C10.2215 1.16675 12.8332 3.77842 12.8332 7.00008V7.87508C12.8332 9.00266 11.9191 9.91675 10.7915 9.91675C10.0891 9.91675 9.46944 9.56196 9.10205 9.02186C8.57145 9.57346 7.82572 9.91675 6.99984 9.91675C5.38901 9.91675 4.08317 8.6109 4.08317 7.00008C4.08317 5.38925 5.38901 4.08341 6.99984 4.08341C7.65655 4.08341 8.26258 4.30047 8.75013 4.66675H9.9165V7.87508C9.9165 8.35831 10.3083 8.75008 10.7915 8.75008C11.2747 8.75008 11.6665 8.35831 11.6665 7.87508V7.00008ZM6.99984 5.25008C6.03331 5.25008 5.24984 6.03356 5.24984 7.00008C5.24984 7.96661 6.03331 8.75008 6.99984 8.75008C7.96636 8.75008 8.74984 7.96661 8.74984 7.00008C8.74984 6.03356 7.96636 5.25008 6.99984 5.25008Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "AtSign"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './AtSign.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AtSign'
export default Icon

View File

@ -3,6 +3,7 @@ export { default as Answer } from './Answer'
export { default as ApiAggregate } from './ApiAggregate'
export { default as Assigner } from './Assigner'
export { default as Asterisk } from './Asterisk'
export { default as AtSign } from './AtSign'
export { default as CalendarCheckLine } from './CalendarCheckLine'
export { default as Code } from './Code'
export { default as Datasource } from './Datasource'

View File

@ -58,10 +58,12 @@ const SubGraphMain: FC<SubGraphMainProps> = (props) => {
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
flowType,
flowId,
interactionMode: InteractionMode.Subgraph,
})
const inspectVarsCrud = useInspectVarsCrudCommon({
flowType,
flowId,
interactionMode: InteractionMode.Subgraph,
})
const handleSyncSubGraphDraft = useCallback(async () => {

View File

@ -0,0 +1,265 @@
import type { Node } from '@/app/components/workflow/types'
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
import { VarInInspectType } from '@/types/workflow'
const buildAliasVarId = (mapping: AgentAliasMapping) => {
const outputPath = mapping.outputSelector.join('.')
return `alias:${mapping.parentNodeId}:${mapping.extractorNodeId}:${outputPath}`
}
type AgentAliasMapping = {
parentNodeId: string
extractorNodeId: string
outputSelector: string[]
aliasName: string
}
type ToolParameterShape = {
value?: unknown
nested_node_config?: {
extractor_node_id?: string
output_selector?: unknown
}
}
type ToolNodeDataShape = {
tool_parameters?: Record<string, ToolParameterShape>
}
type AliasSource = {
sourceVar: VarInInspect
matchedSelector: string[]
resolvedValue: unknown
resolvedValueFound: boolean
usedFallback: boolean
}
const toSelectorKey = (selector: string[]) => selector.join('.')
const resolveOutputSelector = (extractorNodeId: string, rawSelector?: unknown): string[] => {
if (!Array.isArray(rawSelector))
return []
if (rawSelector[0] === extractorNodeId)
return rawSelector.slice(1)
return rawSelector as string[]
}
const collectAgentAliasMappings = (nodes: Node[]) => {
const nodesById = new Map(nodes.map(node => [node.id, node]))
const mappings: AgentAliasMapping[] = []
const extractorNodeIds = new Set<string>()
nodes.forEach((node) => {
if (node.data.type !== BlockEnum.Tool)
return
const toolData = node.data as ToolNodeDataShape
const toolParams = toolData.tool_parameters || {}
Object.entries(toolParams).forEach(([paramKey, param]) => {
const value = param?.value
if (typeof value !== 'string')
return
const matches = Array.from(value.matchAll(AGENT_CONTEXT_VAR_PATTERN))
if (!matches.length)
return
const agentNodeId = getAgentNodeIdFromContextVar(matches[0][0])
if (!agentNodeId)
return
const extractorNodeId = param?.nested_node_config?.extractor_node_id || `${node.id}_ext_${paramKey}`
extractorNodeIds.add(extractorNodeId)
const resolvedOutputSelector = resolveOutputSelector(extractorNodeId, param?.nested_node_config?.output_selector)
const outputSelector = resolvedOutputSelector.length > 0
? resolvedOutputSelector
: ['structured_output', paramKey]
const agentNode = nodesById.get(agentNodeId)
const aliasName = `@${agentNode?.data.title || agentNodeId}`
mappings.push({
parentNodeId: node.id,
extractorNodeId,
outputSelector,
aliasName,
})
})
})
return { mappings, extractorNodeIds, nodesById }
}
const findAliasSourceVar = (vars: VarInInspect[], extractorNodeId: string, outputSelector: string[]) => {
const selectorKey = toSelectorKey(outputSelector)
return vars.find((varItem) => {
const selector = Array.isArray(varItem.selector) ? varItem.selector : []
if (selector[0] !== extractorNodeId)
return false
return toSelectorKey(selector.slice(1)) === selectorKey
})
}
const resolveNestedValue = (value: unknown, path: string[]): { found: boolean, value: unknown } => {
let current: unknown = value
for (const key of path) {
if (Array.isArray(current)) {
const index = Number.parseInt(key, 10)
if (!Number.isNaN(index) && index >= 0 && index < current.length) {
current = current[index]
continue
}
return { found: false, value: undefined }
}
if (current && typeof current === 'object') {
if (Object.prototype.hasOwnProperty.call(current, key)) {
current = (current as Record<string, unknown>)[key]
continue
}
}
return { found: false, value: undefined }
}
return { found: true, value: current }
}
const resolveAliasSourceVar = (
vars: VarInInspect[],
extractorNodeId: string,
outputSelector: string[],
): AliasSource | undefined => {
const directMatch = findAliasSourceVar(vars, extractorNodeId, outputSelector)
if (directMatch) {
return {
sourceVar: directMatch,
matchedSelector: directMatch.selector as string[],
resolvedValue: directMatch.value,
resolvedValueFound: true,
usedFallback: false,
}
}
if (outputSelector.length === 0)
return undefined
const prefixKey = outputSelector[0]
const prefixVar = vars.find((varItem) => {
const selector = Array.isArray(varItem.selector) ? varItem.selector : []
if (selector[0] !== extractorNodeId)
return false
return selector[1] === prefixKey && selector.length === 2
})
if (!prefixVar)
return undefined
if (outputSelector.length === 1) {
return {
sourceVar: prefixVar,
matchedSelector: prefixVar.selector as string[],
resolvedValue: prefixVar.value,
resolvedValueFound: true,
usedFallback: true,
}
}
const resolved = resolveNestedValue(prefixVar.value, outputSelector.slice(1))
return {
sourceVar: prefixVar,
matchedSelector: prefixVar.selector as string[],
resolvedValue: resolved.value,
resolvedValueFound: resolved.found,
usedFallback: true,
}
}
export const applyAgentSubgraphInspectVars = (nodesWithInspectVars: NodeWithVar[], allNodes: Node[]) => {
const hideExtractorNodes = true
const { mappings, extractorNodeIds, nodesById } = collectAgentAliasMappings(allNodes)
if (mappings.length === 0 && extractorNodeIds.size === 0) {
return nodesWithInspectVars
}
const resultMap = new Map<string, NodeWithVar>()
nodesWithInspectVars.forEach((node) => {
const isExtractorNode = extractorNodeIds.has(node.nodeId)
resultMap.set(node.nodeId, {
...node,
vars: [...node.vars],
isHidden: hideExtractorNodes && isExtractorNode,
})
})
const getOrCreateParentNode = (parentNodeId: string): NodeWithVar | undefined => {
const existing = resultMap.get(parentNodeId)
if (existing)
return existing
const parentNode = nodesById.get(parentNodeId)
if (!parentNode)
return undefined
return {
nodeId: parentNode.id,
nodeType: parentNode.data.type,
title: parentNode.data.title,
nodePayload: parentNode.data,
vars: [] as VarInInspect[],
isValueFetched: false,
isHidden: false,
}
}
mappings.forEach((mapping) => {
const parent = getOrCreateParentNode(mapping.parentNodeId)
if (!parent)
return
const parentVars = parent.vars as VarInInspect[]
const aliasId = buildAliasVarId(mapping)
const upsertAliasVar = (aliasVar: VarInInspect, shouldOverwrite: boolean) => {
const existingIndex = parentVars.findIndex(varItem => varItem.id === aliasVar.id)
if (existingIndex === -1)
parentVars.unshift(aliasVar)
else if (shouldOverwrite)
parentVars[existingIndex] = { ...parentVars[existingIndex], ...aliasVar }
}
const placeholderAliasVar: VarInInspect = {
id: aliasId,
type: VarInInspectType.node,
name: mapping.aliasName,
description: '',
selector: [mapping.parentNodeId, mapping.aliasName],
value_type: VarType.any,
value: undefined,
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
aliasMeta: {
extractorNodeId: mapping.extractorNodeId,
outputSelector: mapping.outputSelector,
sourceVarId: aliasId,
},
}
const extractorGroup = nodesWithInspectVars.find(node => node.nodeId === mapping.extractorNodeId)
if (!extractorGroup?.vars?.length) {
upsertAliasVar(placeholderAliasVar, false)
resultMap.set(mapping.parentNodeId, parent)
return
}
const resolved = resolveAliasSourceVar(extractorGroup.vars, mapping.extractorNodeId, mapping.outputSelector)
if (!resolved) {
upsertAliasVar(placeholderAliasVar, false)
resultMap.set(mapping.parentNodeId, parent)
return
}
const resolvedValue = resolved.resolvedValueFound ? resolved.resolvedValue : resolved.sourceVar.value
const aliasVar: VarInInspect = {
...resolved.sourceVar,
id: aliasId,
name: mapping.aliasName,
selector: [mapping.parentNodeId, mapping.aliasName],
value: resolvedValue,
visible: true,
aliasMeta: {
extractorNodeId: mapping.extractorNodeId,
outputSelector: mapping.outputSelector,
sourceVarId: resolved.sourceVar.id,
},
}
upsertAliasVar(aliasVar, true)
resultMap.set(mapping.parentNodeId, parent)
})
// TODO: handle assemble sub-graph output mapping.
return Array.from(resultMap.values())
}

View File

@ -4,6 +4,7 @@ import type { FlowType } from '@/types/common'
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { InteractionMode } from '@/app/components/workflow'
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import {
@ -16,15 +17,18 @@ import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@
import { fetchAllInspectVars } from '@/service/workflow'
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
flowType: FlowType
flowId: string
interactionMode?: InteractionMode
}
export const useSetWorkflowVarsWithValue = ({
flowType,
flowId,
interactionMode,
}: Params) => {
const workflowStore = useWorkflowStore()
const store = useStoreApi()
@ -94,7 +98,10 @@ export const useSetWorkflowVarsWithValue = ({
}
return nodeWithVar
})
setNodesWithInspectVars(res)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(res, nodeArr) : res
setNodesWithInspectVars(nextNodes)
}
const fetchInspectVars = useCallback(async (params: {

View File

@ -5,6 +5,7 @@ import type { VarInInspect } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { InteractionMode } from '@/app/components/workflow'
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import {
@ -23,14 +24,17 @@ import {
} from '@/service/use-tools'
import { fetchNodeInspectVars } from '@/service/workflow'
import { VarInInspectType } from '@/types/workflow'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
flowId: string
flowType: FlowType
interactionMode?: InteractionMode
}
export const useInspectVarsCrudCommon = ({
flowId,
flowType,
interactionMode,
}: Params) => {
const workflowStore = useWorkflowStore()
const store = useStoreApi()
@ -108,6 +112,7 @@ export const useInspectVarsCrudCommon = ({
const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => {
const {
setNodeInspectVars,
setNodesWithInspectVars,
dataSourceList,
} = workflowStore.getState()
const nodeId = selector[0]
@ -141,7 +146,13 @@ export const useInspectVarsCrudCommon = ({
}
})
setNodeInspectVars(nodeId, varsWithSchemaType)
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools])
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
if (resolvedInteractionMode !== InteractionMode.Subgraph) {
const { nodesWithInspectVars } = workflowStore.getState()
const nextNodes = applyAgentSubgraphInspectVars(nodesWithInspectVars, nodeArr)
setNodesWithInspectVars(nextNodes)
}
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode])
// after last run would call this
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
@ -169,9 +180,12 @@ export const useInspectVarsCrudCommon = ({
}
}
})
setNodesWithInspectVars(nodes)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(nodes, allNodes) : nodes
setNodesWithInspectVars(nextNodes)
handleCancelNodeSuccessStatus(nodeId)
}, [workflowStore, handleCancelNodeSuccessStatus])
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode])
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
const { nodesWithInspectVars } = workflowStore.getState()

View File

@ -18,15 +18,7 @@ import { FlowType } from '@/types/common'
// Constants
export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
export const getAgentNodeIdFromContextVar = (placeholder: string): string => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
export { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
export const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string): string => {
if (!toolNodeId || !paramKey)

View File

@ -15,19 +15,12 @@ import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { BlockEnum } from '@/app/components/workflow/types'
import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
import { useGetLanguage } from '@/context/i18n'
import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames'
import { VarType } from './types'
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
const getAgentNodeIdFromContextVar = (placeholder: string) => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
type AgentCheckValidContext = {
provider?: StrategyPluginDetail
strategy?: StrategyDetail

View File

@ -0,0 +1,9 @@
export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
export const getAgentNodeIdFromContextVar = (placeholder: string): string => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}

View File

@ -11,6 +11,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import { AtSign } from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import BlockIcon from '@/app/components/workflow/block-icon'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
@ -147,24 +148,32 @@ const Group = ({
{/* var item list */}
{!isCollapsed && !nodeData?.isSingRunRunning && (
<div className="px-0.5">
{visibleVarList.length > 0 && visibleVarList.map(varItem => (
<div
key={varItem.id}
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
)}
onClick={() => handleSelectVar(varItem, varType)}
>
<VariableIconWithColor
variableCategory={varType}
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
className="size-4"
/>
<div className="system-sm-medium grow truncate text-text-secondary">{varItem.name}</div>
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
</div>
))}
{visibleVarList.length > 0 && visibleVarList.map((varItem) => {
const isAgentAliasVar = typeof varItem.name === 'string' && varItem.name.startsWith('@')
const displayName = isAgentAliasVar ? varItem.name.slice(1) : varItem.name
return (
<div
key={varItem.id}
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
)}
onClick={() => handleSelectVar(varItem, varType)}
>
{isAgentAliasVar
? <AtSign className="size-4 shrink-0 text-util-colors-violet-violet-600" />
: (
<VariableIconWithColor
variableCategory={varType}
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
className="size-4"
/>
)}
<div className="system-sm-medium grow truncate text-text-secondary">{displayName}</div>
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
</div>
)
})}
</div>
)}
</div>

View File

@ -36,6 +36,8 @@ const Left = ({
} = useCurrentVars()
const { handleNodeSelect } = useNodesInteractions()
const visibleNodesWithInspectVars = nodesWithInspectVars.filter(node => !node.isHidden)
const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0
const handleClearAll = () => {
@ -91,7 +93,7 @@ const Left = ({
</div>
)}
{/* group nodes */}
{nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => (
{visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => (
<Group
key={group.nodeId}
varType={VarInInspectType.node}

View File

@ -19,14 +19,15 @@ import Empty from './empty'
import Left from './left'
import Listening from './listening'
import Right from './right'
import { toEnvVarInInspect } from './utils'
export type currentVarType = {
nodeId: string
nodeType: string
title: string
isValueFetched?: boolean
var: VarInInspect
nodeData: NodeProps['data']
var?: VarInInspect
nodeData?: NodeProps['data']
}
const Panel: FC = () => {
@ -59,23 +60,12 @@ const Panel: FC = () => {
return
if (currentFocusNodeId === VarInInspectType.environment) {
const currentVar = environmentVariables.find(v => v.id === currentVarId)
const res = {
return {
nodeId: VarInInspectType.environment,
title: VarInInspectType.environment,
nodeType: VarInInspectType.environment,
var: currentVar ? toEnvVarInInspect(currentVar) : undefined,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.environment,
visible: true,
...(currentVar.value_type === 'secret' ? { value: '******************' } : {}),
},
}
}
return res
}
if (currentFocusNodeId === VarInInspectType.conversation) {
const currentVar = conversationVars.find(v => v.id === currentVarId)
@ -83,15 +73,12 @@ const Panel: FC = () => {
nodeId: VarInInspectType.conversation,
title: VarInInspectType.conversation,
nodeType: VarInInspectType.conversation,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.conversation,
},
}
var: currentVar
? {
...currentVar,
type: VarInInspectType.conversation,
}
: undefined,
}
return res
}
@ -101,15 +88,12 @@ const Panel: FC = () => {
nodeId: VarInInspectType.system,
title: VarInInspectType.system,
nodeType: VarInInspectType.system,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.system,
},
}
var: currentVar
? {
...currentVar,
type: VarInInspectType.system,
}
: undefined,
}
return res
}
@ -124,22 +108,32 @@ const Panel: FC = () => {
isSingRunRunning: targetNode.isSingRunRunning,
isValueFetched: targetNode.isValueFetched,
nodeData: targetNode.nodePayload,
...(currentVar ? { var: currentVar } : {}),
var: currentVar,
}
}, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
const currentAliasMeta = useMemo(() => {
if (!currentFocusNodeId || !currentVarId)
return undefined
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
const targetVar = targetNode?.vars.find(v => v.id === currentVarId)
return targetVar?.aliasMeta
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars])
const fetchNodeId = currentAliasMeta?.extractorNodeId || currentFocusNodeId
const isCurrentNodeVarValueFetching = useMemo(() => {
if (!currentNodeInfo)
if (!fetchNodeId)
return false
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentNodeInfo.nodeId)
const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId)
if (!targetNode)
return false
return !targetNode.isValueFetched
}, [currentNodeInfo, nodesWithInspectVars])
}, [fetchNodeId, nodesWithInspectVars])
const handleNodeVarSelect = useCallback((node: currentVarType) => {
setCurrentFocusNodeId(node.nodeId)
setCurrentVarId(node.var.id)
if (node.var)
setCurrentVarId(node.var.id)
}, [setCurrentFocusNodeId, setCurrentVarId])
const { isLoading, schemaTypeDefinitions } = useMatchSchemaType()
@ -150,12 +144,12 @@ const Panel: FC = () => {
}, [eventEmitter])
useEffect(() => {
if (currentFocusNodeId && currentVarId && !isLoading) {
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
if (currentFocusNodeId && currentVarId && !isLoading && fetchNodeId) {
const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId)
if (targetNode && !targetNode.isValueFetched)
fetchInspectVarValue([currentFocusNodeId], schemaTypeDefinitions!)
fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!)
}
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading])
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading, fetchNodeId])
if (isListening) {
return (

View File

@ -55,8 +55,14 @@ const Right = ({
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
const toolIcon = useToolIcon(currentNodeVar?.nodeData)
const isTruncated = currentNodeVar?.var.is_truncated
const fullContent = currentNodeVar?.var.full_content
const currentVar = currentNodeVar?.var
const currentNodeType = currentNodeVar?.nodeType
const currentNodeTitle = currentNodeVar?.title
const currentNodeId = currentNodeVar?.nodeId
const isTruncated = !!currentVar?.is_truncated
const fullContent = currentVar?.full_content
const isAgentAliasVar = currentVar?.name?.startsWith('@')
const displayVarName = isAgentAliasVar ? currentVar?.name?.slice(1) : currentVar?.name
const {
resetConversationVar,
@ -65,15 +71,15 @@ const Right = ({
} = useCurrentVars()
const handleValueChange = (varId: string, value: any) => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
editInspectVarValue(currentNodeVar.nodeId, varId, value)
}
const resetValue = () => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
resetToLastRunVar(currentNodeVar.nodeId, currentNodeVar.var.id)
resetToLastRunVar(currentNodeVar.nodeId, currentVar.id)
}
const handleClose = () => {
@ -82,13 +88,13 @@ const Right = ({
}
const handleClear = () => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
resetConversationVar(currentNodeVar.var.id)
resetConversationVar(currentVar.id)
}
const getCopyContent = () => {
const value = currentNodeVar?.var.value
const value = currentVar?.value
if (value === null || value === undefined)
return ''
@ -160,8 +166,8 @@ const Right = ({
handleHidePromptGenerator()
}, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator])
const schemaType = currentNodeVar?.var.schemaType
const valueType = currentNodeVar?.var.value_type
const schemaType = currentVar?.schemaType
const valueType = currentVar?.value_type
const valueTypeLabel = formatVarTypeLabel(valueType)
const shouldShowSchemaType = !!schemaType
&& schemaType !== valueType
@ -178,32 +184,34 @@ const Right = ({
</ActionButton>
)}
<div className="flex w-0 grow items-center gap-1">
{currentNodeVar?.var && (
{currentVar && (
<>
{
[VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && (
currentNodeType
&& [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeType as VarInInspectType) && (
<VariableIconWithColor
variableCategory={currentNodeVar.nodeType as VarInInspectType}
variableCategory={currentNodeType as VarInInspectType}
className="size-4"
/>
)
}
{currentNodeVar.nodeType !== VarInInspectType.environment
&& currentNodeVar.nodeType !== VarInInspectType.conversation
&& currentNodeVar.nodeType !== VarInInspectType.system
{currentNodeType
&& currentNodeType !== VarInInspectType.environment
&& currentNodeType !== VarInInspectType.conversation
&& currentNodeType !== VarInInspectType.system
&& (
<>
<BlockIcon
className="shrink-0"
type={currentNodeVar.nodeType as BlockEnum}
type={currentNodeType as BlockEnum}
size="xs"
toolIcon={toolIcon}
/>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeVar.title}</div>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeTitle}</div>
<div className="system-sm-regular shrink-0 text-text-quaternary">/</div>
</>
)}
<div title={currentNodeVar.var.name} className="system-sm-semibold truncate text-text-secondary">{currentNodeVar.var.name}</div>
<div title={displayVarName} className="system-sm-semibold truncate text-text-secondary">{displayVarName}</div>
<div className="system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary">
<span>{`${valueTypeLabel}${displaySchemaType}`}</span>
{isTruncated && (
@ -221,7 +229,7 @@ const Right = ({
)}
</div>
<div className="flex shrink-0 items-center gap-1">
{currentNodeVar && (
{currentVar && (
<>
{canShowPromptGenerator && (
<Tooltip popupContent={t('generate.optimizePromptTooltip', { ns: 'appDebug' })}>
@ -245,27 +253,27 @@ const Right = ({
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentNodeVar.var.edited && (
{!isTruncated && currentVar.edited && (
<Badge>
<span className="ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary"></span>
<span className="system-2xs-semibold-uupercase">{t('debug.variableInspect.edited', { ns: 'workflow' })}</span>
</Badge>
)}
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && (
{!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.reset', { ns: 'workflow' })}>
<ActionButton onClick={resetValue}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && (
{!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })}>
<ActionButton onClick={handleClear}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{currentNodeVar.var.value_type !== 'secret' && (
{currentVar.value_type !== 'secret' && (
<CopyFeedback content={getCopyContent()} />
)}
</>
@ -277,16 +285,16 @@ const Right = ({
</div>
{/* content */}
<div className="grow p-2">
{!currentNodeVar?.var && <Empty />}
{!currentVar && <Empty />}
{isValueFetching && (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)}
{currentNodeVar?.var && !isValueFetching && (
{currentVar && currentNodeId && !isValueFetching && (
<ValueContent
key={`${currentNodeVar.nodeId}-${currentNodeVar.var.id}`}
currentVar={currentNodeVar.var}
key={`${currentNodeId}-${currentVar.id}`}
currentVar={currentVar}
handleValueChange={handleValueChange}
isTruncated={!!isTruncated}
/>

View File

@ -1,4 +1,8 @@
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import type { VarInInspect } from '@/types/workflow'
import { z } from 'zod'
import { VarType } from '@/app/components/workflow/types'
import { VarInInspectType } from '@/types/workflow'
const arrayStringSchemaParttern = z.array(z.string())
const arrayNumberSchemaParttern = z.array(z.number())
@ -10,6 +14,34 @@ type Json = Literal | { [key: string]: Json } | Json[]
const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
const arrayJsonSchema: z.ZodType<Json[]> = z.lazy(() => z.array(jsonSchema))
const toEnvVarType = (valueType: EnvironmentVariable['value_type']): VarInInspect['value_type'] => {
switch (valueType) {
case 'number':
return VarType.number
case 'secret':
return VarType.secret
default:
return VarType.string
}
}
export const toEnvVarInInspect = (envVar: EnvironmentVariable): VarInInspect => {
const valueType = envVar.value_type
return {
id: envVar.id,
type: VarInInspectType.environment,
name: envVar.name,
description: envVar.description,
selector: [VarInInspectType.environment, envVar.name],
value_type: toEnvVarType(valueType),
value: valueType === 'secret' ? '******************' : envVar.value,
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
}
}
export const validateJSONSchema = (schema: any, type: string) => {
if (type === 'array[string]') {
const result = arrayStringSchemaParttern.safeParse(schema)

View File

@ -477,6 +477,12 @@ export type FullContent = {
download_url: string
}
export type VarInInspectAliasMeta = {
extractorNodeId: string
outputSelector: string[]
sourceVarId: string
}
export type VarInInspect = {
id: string
type: VarInInspectType
@ -490,6 +496,7 @@ export type VarInInspect = {
is_truncated: boolean
full_content: FullContent
schemaType?: string
aliasMeta?: VarInInspectAliasMeta
}
export type NodeWithVar = {
@ -500,4 +507,5 @@ export type NodeWithVar = {
vars: VarInInspect[]
isSingRunRunning?: boolean
isValueFetched?: boolean
isHidden?: boolean
}