mirror of
https://github.com/langgenius/dify.git
synced 2026-05-19 08:17:14 +08:00
fix(workflow): remove legacy userinput files alias
This commit is contained in:
@ -14,6 +14,7 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
_LEGACY_SYSTEM_NODE_ID = "sys"
|
||||
_LEGACY_USER_INPUT_NODE_ID = "userinput"
|
||||
_LEGACY_FILES_VARIABLE = "files"
|
||||
_COMPAT_VARIABLE_PREFIX = "sys_files"
|
||||
_COMPAT_VARIABLE_DESCRIPTION = "Compatibility input for deprecated sys.files."
|
||||
@ -21,7 +22,7 @@ _FILE_LIST_TYPE = "file-list"
|
||||
_DEFAULT_FILE_NUMBER_LIMITS = 3
|
||||
_DEFAULT_ALLOWED_FILE_UPLOAD_METHODS = ["local_file", "remote_url"]
|
||||
_DEFAULT_ALLOWED_FILE_TYPES = ["image", "document", "audio", "video"]
|
||||
_SYS_FILES_TEMPLATE_PATTERN = re.compile(r"\{\{#sys\.files#\}\}")
|
||||
_LEGACY_FILES_TEMPLATE_PATTERN = re.compile(r"\{\{#(?:sys|userinput)\.files#\}\}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -41,7 +42,7 @@ def migrate_legacy_sys_files_graph(
|
||||
*,
|
||||
features: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a graph where `sys.files` references point to a Start-node file-list variable."""
|
||||
"""Return a graph where legacy file-system references point to a Start-node file-list variable."""
|
||||
|
||||
return migrate_legacy_sys_files_graph_with_result(graph, features=features).graph
|
||||
|
||||
@ -199,7 +200,7 @@ def _contains_legacy_sys_files_reference(value: Any) -> bool:
|
||||
return True
|
||||
|
||||
if isinstance(value, str):
|
||||
return bool(_SYS_FILES_TEMPLATE_PATTERN.search(value))
|
||||
return bool(_LEGACY_FILES_TEMPLATE_PATTERN.search(value))
|
||||
|
||||
if isinstance(value, Mapping):
|
||||
return any(_contains_legacy_sys_files_reference(item) for item in value.values())
|
||||
@ -215,7 +216,7 @@ def _replace_legacy_sys_files_references(value: Any, *, start_node_id: str, vari
|
||||
return [start_node_id, variable_name]
|
||||
|
||||
if isinstance(value, str):
|
||||
return _SYS_FILES_TEMPLATE_PATTERN.sub(f"{{{{#{start_node_id}.{variable_name}#}}}}", value)
|
||||
return _LEGACY_FILES_TEMPLATE_PATTERN.sub(f"{{{{#{start_node_id}.{variable_name}#}}}}", value)
|
||||
|
||||
if isinstance(value, Mapping):
|
||||
return {
|
||||
@ -244,7 +245,7 @@ def _is_legacy_sys_files_selector(value: Any) -> bool:
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) == 2
|
||||
and value[0] == _LEGACY_SYSTEM_NODE_ID
|
||||
and value[0] in (_LEGACY_SYSTEM_NODE_ID, _LEGACY_USER_INPUT_NODE_ID)
|
||||
and value[1] == _LEGACY_FILES_VARIABLE
|
||||
)
|
||||
|
||||
|
||||
@ -7,9 +7,12 @@ from core.workflow.legacy_system_files import (
|
||||
)
|
||||
|
||||
_LEGACY_NODE_ID = "sys"
|
||||
_LEGACY_ALIAS_NODE_ID = "userinput"
|
||||
_LEGACY_VARIABLE_NAME = "files"
|
||||
_LEGACY_SELECTOR = [_LEGACY_NODE_ID, _LEGACY_VARIABLE_NAME]
|
||||
_LEGACY_TEMPLATE = "{{#" + ".".join((_LEGACY_NODE_ID, _LEGACY_VARIABLE_NAME)) + "#}}"
|
||||
_LEGACY_ALIAS_SELECTOR = [_LEGACY_ALIAS_NODE_ID, _LEGACY_VARIABLE_NAME]
|
||||
_LEGACY_ALIAS_TEMPLATE = "{{#" + ".".join((_LEGACY_ALIAS_NODE_ID, _LEGACY_VARIABLE_NAME)) + "#}}"
|
||||
|
||||
|
||||
def test_migrate_legacy_sys_files_graph_ignores_invalid_or_unrelated_graphs():
|
||||
@ -53,6 +56,28 @@ def test_migrate_legacy_sys_files_graph_creates_collision_free_file_input_from_f
|
||||
assert result.graph["nodes"][1]["data"]["answer"] == ["start", "sys_files_1"]
|
||||
|
||||
|
||||
def test_migrate_legacy_sys_files_graph_rewrites_userinput_files_alias_to_same_start_input():
|
||||
graph = {
|
||||
"nodes": [
|
||||
{"id": "start", "data": {"type": "start", "variables": []}},
|
||||
{
|
||||
"id": "answer",
|
||||
"data": {
|
||||
"type": "answer",
|
||||
"answer": _LEGACY_ALIAS_SELECTOR,
|
||||
"template": _LEGACY_ALIAS_TEMPLATE,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
result = migrate_legacy_sys_files_graph_with_result(graph)
|
||||
|
||||
assert result.changed
|
||||
assert result.graph["nodes"][1]["data"]["answer"] == ["start", "sys_files"]
|
||||
assert result.graph["nodes"][1]["data"]["template"] == "{{#start.sys_files#}}"
|
||||
|
||||
|
||||
def test_resolve_legacy_sys_files_compat_variable_handles_missing_start_variable():
|
||||
assert resolve_legacy_sys_files_compat_variable({}) is None
|
||||
assert resolve_legacy_sys_files_compat_variable({"nodes": [1, {"data": {"value": _LEGACY_SELECTOR}}]}) is None
|
||||
|
||||
@ -8,6 +8,7 @@ import Panel from '../panel'
|
||||
const mockUseConfig = vi.hoisted(() => vi.fn())
|
||||
const mockConfigVarModal = vi.hoisted(() => vi.fn())
|
||||
const mockRemoveEffectVarConfirm = vi.hoisted(() => vi.fn())
|
||||
const legacyFilesVariable = ['userinput', 'files'].join('.')
|
||||
|
||||
vi.mock('../use-config', () => ({
|
||||
__esModule: true,
|
||||
@ -90,7 +91,7 @@ describe('StartPanel', () => {
|
||||
render(<Panel id="start-node" data={createData()} panelProps={{} as PanelProps} />)
|
||||
|
||||
expect(screen.getByText('userinput.query')).toBeInTheDocument()
|
||||
expect(screen.getByText('userinput.files')).toBeInTheDocument()
|
||||
expect(screen.queryByText(legacyFilesVariable)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('LEGACY')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add workflow.nodes.start.inputField' }))
|
||||
@ -116,7 +117,8 @@ describe('StartPanel', () => {
|
||||
render(<Panel id="start-node" data={createData()} panelProps={{} as PanelProps} />)
|
||||
|
||||
expect(screen.queryByText('userinput.query')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('LEGACY')).toBeInTheDocument()
|
||||
expect(screen.queryByText(legacyFilesVariable)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('LEGACY')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('remove-confirm')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'confirm-add-var' }))
|
||||
|
||||
@ -6,12 +6,19 @@ import { useTranslation } from 'react-i18next'
|
||||
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
|
||||
import VarItem from './components/var-item'
|
||||
import VarList from './components/var-list'
|
||||
import useConfig from './use-config'
|
||||
|
||||
const i18nPrefix = 'nodes.start'
|
||||
const chatQueryInputVar: InputVar = {
|
||||
variable: 'userinput.query',
|
||||
label: '',
|
||||
type: InputVarType.textInput,
|
||||
required: false,
|
||||
}
|
||||
|
||||
const Panel: FC<NodePanelProps<StartNodeType>> = ({
|
||||
id,
|
||||
@ -67,35 +74,22 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
|
||||
/>
|
||||
|
||||
<div className="mt-1 space-y-1">
|
||||
<Split className="my-2" />
|
||||
{
|
||||
isChatMode && (
|
||||
<VarItem
|
||||
readonly
|
||||
payload={{
|
||||
variable: 'userinput.query',
|
||||
} as any}
|
||||
rightContent={(
|
||||
<div className="text-xs font-normal text-text-tertiary">
|
||||
String
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<Split className="my-2" />
|
||||
<VarItem
|
||||
readonly
|
||||
payload={chatQueryInputVar}
|
||||
rightContent={(
|
||||
<div className="text-xs font-normal text-text-tertiary">
|
||||
String
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<VarItem
|
||||
readonly
|
||||
showLegacyBadge={!isChatMode}
|
||||
payload={{
|
||||
variable: 'userinput.files',
|
||||
} as any}
|
||||
rightContent={(
|
||||
<div className="text-xs font-normal text-text-tertiary">
|
||||
Array[File]
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user