fix(workflow): remove legacy userinput files alias

This commit is contained in:
-LAN-
2026-05-11 15:03:40 +08:00
parent d2d2219271
commit a55beb35fa
4 changed files with 54 additions and 32 deletions

View File

@ -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
)

View File

@ -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

View File

@ -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' }))

View File

@ -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>