Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ def end_convert(node: Node) -> dsl.Component:
response_template="",
)
else:
# A plugin node may emit a non-string payload (dict / list / number).
# Normalize it to a string so the End node's response_template stays
# serializable and the second dialogue round loads correctly (#1282).
raw_content = content.content
response_template = raw_content if isinstance(raw_content, str) else str(raw_content or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_sha: 7e71b1c1eec5cd649b284080b3bfe17006013769

🟡 Medium Priority

变更行:end.py 第 60 行 str(raw_content or "")。当插件输出 falsy 非字符串值时(False00.0[]{}),表达式 raw_content or "" 会因 or 短路求值直接返回 "",导致 str("") = "",实际应当输出 "False""0""0.0""[]""{}"。Issue #​1282 明确提到的布尔值和数字均受此影响:False0 会静默丢失。

触发条件:插件节点向 End 节点输出 False / 0 / 0.0 / [] / {} 等 falsy 非 None 值。
失败模式:response_template 被设为空字符串,而非正确的字符串表示,对话历史中内容丢失。

修复方向:将 None 检查与 str() 转换分开,避免 or 吞掉 falsy 非 None 值。

建议:将 None 检查与 str() 转换解耦:先判 None → 空串,再判 str → 透传,其余 str() 转换。例如:response_template = "" if raw_content is None else raw_content if isinstance(raw_content, str) else str(raw_content)

Suggested change
response_template = raw_content if isinstance(raw_content, str) else str(raw_content or "")
response_template = "" if raw_content is None else raw_content if isinstance(raw_content, str) else str(raw_content)

configs = dsl.EndConfig(
stream_output=inputs.streaming,
response_template=content.content,
response_template=response_template,
)

end_node = dsl.Component(
Expand Down
5 changes: 4 additions & 1 deletion backend/openjiuwen_studio/schemas/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ class LoopParam(BaseType):


class Content(BaseType):
content: Optional[str] = Field("", alias="content")
# ``content`` may carry a non-string payload (dict / list / number) when a
# plugin node feeds the End node directly; consumers normalize it to a
# string, so keep the field permissive (#1282).
content: Optional[Any] = Field("", alias="content")
streaming: Optional[bool] = Field(False, alias="streaming")


Expand Down
79 changes: 79 additions & 0 deletions backend/tests/test_end_node_nonstring_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.

"""Unit tests for End node conversion with non-string content (issue #1282).

A plugin node may emit a non-string payload (dict / list / number) into the
End node's ``content``. ``end_convert`` must normalize it to a string so the
``response_template`` stays serializable and the second dialogue round loads.
"""

import pytest

from openjiuwen_studio.core.manager.convertor.components.end import end_convert
from openjiuwen_studio.core.common.dsl import ComponentType
from openjiuwen_studio.schemas.node import (
Content,
Inputs,
Meta,
Node,
NodeData,
NodePosition,
)


def _make_end_node(content_value) -> Node:
"""Build an End node whose content carries the given payload."""
return Node(
id="end-1",
meta=Meta(position=NodePosition(x=0, y=0), node_id="end-1", name="End", type="end"),
data=NodeData(
title="End",
inputs=Inputs(
input_parameters={},
content=Content(content=content_value),
streaming=False,
),
),
)


def test_end_convert_accepts_string_content():
node = _make_end_node("hello world")
component = end_convert(node)
assert component.type == ComponentType.COMPONENT_TYPE_END
assert component.configs["response_template"] == "hello world"


def test_end_convert_normalizes_dict_content_to_string():
node = _make_end_node({"key": "value"})
component = end_convert(node)
assert component.type == ComponentType.COMPONENT_TYPE_END
template = component.configs["response_template"]
assert isinstance(template, str)
assert "key" in template


def test_end_convert_normalizes_number_content_to_string():
node = _make_end_node(12345)
component = end_convert(node)
template = component.configs["response_template"]
assert isinstance(template, str)
assert template == "12345"


def test_end_convert_handles_none_content_gracefully():
node = _make_end_node(None)
component = end_convert(node)
assert component.configs["response_template"] == ""


def test_end_convert_keeps_serializable_configs_for_non_string_content():
# Regression guard: the dumped configs must be JSON-serializable even when
# the raw payload was a dict (the second dialogue round re-reads these).
import json

node = _make_end_node({"a": [1, 2, 3]})
component = end_convert(node)
json.dumps(component.configs) # must not raise
assert isinstance(component.configs["response_template"], str)