diff --git a/app/workflow/actions/invoke_plugin.py b/app/workflow/actions/invoke_plugin.py index 9bff90795..3d1cefdc5 100644 --- a/app/workflow/actions/invoke_plugin.py +++ b/app/workflow/actions/invoke_plugin.py @@ -47,7 +47,16 @@ class InvokePluginAction(BaseAction): logger.error(f"插件不存在: {params.plugin_id}") return context actions = plugin_actions[0].get("actions", []) - action = next((action for action in actions if action.get("action_id") == params.action_id), None) + # 插件公开动作契约使用 ``id``;读取旧插件声明时保留 action_id 回退,避免已保存工作流失效。 + action = next( + ( + action + for action in actions + if (action.get("id") or action.get("action_id")) + == params.action_id + ), + None, + ) if not action or not action.get("func"): logger.error(f"插件动作不存在: {params.plugin_id} - {params.action_id}") return context diff --git a/tests/test_workflow_invoke_plugin.py b/tests/test_workflow_invoke_plugin.py new file mode 100644 index 000000000..0e8938b18 --- /dev/null +++ b/tests/test_workflow_invoke_plugin.py @@ -0,0 +1,54 @@ +"""插件工作流动作标识的公开契约与执行回归。""" + +from unittest.mock import Mock, patch + +from app.schemas.workflow import ActionContext +from app.workflow.actions.invoke_plugin import InvokePluginAction + + +def _execute_with_action(action: dict) -> tuple[InvokePluginAction, ActionContext]: + """在最小运行时替身中执行一个插件动作。""" + context = ActionContext(content="before") + action_fn = Mock(return_value=(True, context)) + action["func"] = action_fn + plugin_manager = Mock() + plugin_manager.get_plugin_actions.return_value = [ + {"plugin_id": "plugin-a", "actions": [action]} + ] + + with patch( + "app.workflow.actions.get_configured_system_config", + return_value=Mock(), + ), patch( + "app.workflow.actions.invoke_plugin.get_plugin_manager", + return_value=plugin_manager, + ): + action_runner = InvokePluginAction("invoke") + result = action_runner.execute( + workflow_id=1, + params={ + "plugin_id": "plugin-a", + "action_id": "cleanup", + "action_params": {"force": True}, + }, + context=context, + ) + + action_fn.assert_called_once_with(context, force=True) + assert action_runner.success is True + assert action_runner.done is True + return action_runner, result + + +def test_invoke_plugin_uses_public_action_id() -> None: + """插件公开的 id 字段必须可被工作流执行器解析。""" + _, result = _execute_with_action({"id": "cleanup"}) + + assert result.content == "before" + + +def test_invoke_plugin_keeps_legacy_action_id_fallback() -> None: + """历史插件声明仍可通过 action_id 回退执行。""" + _, result = _execute_with_action({"action_id": "cleanup"}) + + assert result.content == "before"