#------------------------------------------------------------------------
# do NOT modify this file !
#------------------------------------------------------------------------

import os, json, re, inspect, html
from typing import TypedDict, Any, Callable
from typing_extensions import NotRequired
import graphviz
import unicodedata
from varname import varname
from typeguard import check_type, TypeCheckError

#------------------------------------------------------------------------

_chattree_def_lang_sgn = ""
with open("./conf/public.conf","rt",encoding="utf-8") as f:
    line = f.readline()
    while len(line) > 0:
        if line.find("/////") >= 0:
            line = line[:line.find("/////")]
        line = line.strip()
        if re.match(r"^LANG\s*=", line):
            _chattree_def_lang_sgn = line.split("=")[1].strip()[1:-1]
            break
        line = f.readline()
def chattree_def_lang_sgn():
    global _chattree_def_lang_sgn
    assert _chattree_def_lang_sgn in ["cn","en","es","de","ru","ja","fr","pt"]
    return _chattree_def_lang_sgn
def chattree_def_lang_str(cn, en, es="", de="", ru="", ja="", fr="", pt=""):
    global _chattree_def_lang_sgn
    assert _chattree_def_lang_sgn in ["cn","en","es","de","ru","ja","fr","pt"]
    if _chattree_def_lang_sgn == "cn":
        ret_val = cn
    elif _chattree_def_lang_sgn == "en":
        ret_val = en
    elif _chattree_def_lang_sgn == "es":
        ret_val = es
    elif _chattree_def_lang_sgn == "de":
        ret_val = de
    elif _chattree_def_lang_sgn == "ru":
        ret_val = ru
    elif _chattree_def_lang_sgn == "ja":
        ret_val = ja
    elif _chattree_def_lang_sgn == "fr":
        ret_val = fr
    elif _chattree_def_lang_sgn == "pt":
        ret_val = pt
    return ret_val

def chattree_def_compact_json_dumps(d):
    func_def_str = ""
    def function_serializer(obj):
        nonlocal func_def_str
        if callable(obj):
            if obj.__name__ == "<lambda>":
                try:
                    source = (
                        inspect.getsource(obj).strip()[
                            inspect.getsource(obj).strip().find("lambda"):
                        ]
                    )
                    return source
                except Exception:
                    return "<unknown_lambda>"
            else:
                func_source = inspect.getsource(obj).strip()
                func_def_str += ("\n\n#" + "-"*(len(func_source.split("\n")[0])-1) + "\n" + func_source)
                return obj.__name__ + "()"
        raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")    
    json_str = json.dumps(d, indent=4, separators=(',', ':'), ensure_ascii=False, default=function_serializer)
    json_str = re.sub(
        r'\[\s*([^\[\]\{\}]+?)\s*\]', 
        lambda m: '[' + re.sub(r'\s*\n\s*', '', m.group(1)) + ']', 
        json_str
    )
    json_str = re.sub(r"\n\s*\{\s*\n", "{\n", json_str)
    json_str = json_str.strip()
    assert (json_str[0] == "{" and json_str[-1] == "}") or (json_str[0] == "[" and json_str[-1] == "]")
    json_str = json_str[1:-1].strip()
    json_str = json_str.replace("\n    ","\n")
    json_str = json_str.replace("\\n","\\\\n")
    # json_str = "==>" + json_str.replace("\n\"","\n==>\"")
    return json_str + func_def_str

def chattree_def_display_len(text):
    length = 0
    for char in text:
        # east_asian_width 返回字符宽度属性
        # 'W' (Wide) 和 'F' (Full-width) 代表宽字符（中文、全角符号等）
        # 'A' (Ambiguous) 通常在东亚环境下也算宽，视情况可加进来
        if unicodedata.east_asian_width(char) in ('F', 'W'):
            length += 2
        else:
            length += 1
    return length

#------------------------------------------------------------------------

ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE = { "cn":"执行前", "en":"before_execution" }
ALL_NODE_AFTER_EXECUTION_ATTRIBUTE  = { "cn":"执行后", "en":"after_execution" }
ALL_NODE_COMMENT_ATTRIBUTE  = { "cn":"节点注释", "en":"node_comment" }

START_NODE        = { "cn":"开始", "en":"start" }
START_NODE_CHATTREE_TITLE_ATTRIBUTE                = { "cn":"对话树标题", "en":"chattree_title" } 
START_NODE_SYSTEM_ROLE_ATTRIBUTE                   = { "cn":"系统角色", "en":"system_role" } 
START_NODE_USER_ROLE_ATTRIBUTE                     = { "cn":"用户角色", "en":"user_role" }
START_NODE_BACKGROUND_INFORMATION_ATTRIBUTE        = { "cn":"背景信息", "en":"background_information" }
START_NODE_ALLOW_TRANSFER_HUMAN_AGENT_ATTRIBUTE          = { "cn":"是否允许转接人工", "en":"allow_transfer_human_agent" }
START_NODE_STATIC_REFERENCE_INFORMATION_ATTRIBUTE  = { "cn":"静态参考信息", "en":"static_reference_information" }
START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE = { "cn":"动态参考信息", "en":"dynamic_reference_information" }
START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_INTENT_FIELD            = { "cn":"意图",    "en":"intent" }              # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_INTENT_CONSTRAINT_FIELD = { "cn":"意图约束", "en":"intent_constraint" }  # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_FUNCTION_FIELD          = { "cn":"函数",     "en":"function" }          # 仅用于 python 对话树，xmind 对话树是其它表达方式, <对话树>.xmind 在内部是待执行的 script
START_NODE_INTENT_TRIGGER_ATTRIBUTE = { "cn":"意图触发", "en":"intent_trigger" }
START_NODE_INTENT_TRIGGER_ATTRIBUTE_INTENT_FIELD            = { "cn":"意图",    "en":"intent" }              # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_INTENT_TRIGGER_ATTRIBUTE_INTENT_CONSTRAINT_FIELD = { "cn":"意图约束", "en":"intent_constraint" }  # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_INTENT_TRIGGER_ATTRIBUTE_FUNCTION_FIELD          = { "cn":"函数",     "en":"function" }          # 仅用于 python 对话树，xmind 对话树是其它表达方式, <对话树>.xmind 在内部是待执行的 script
START_NODE_CODE_FILE_ATTRIBUTE                     = { "cn":"代码文件", "en":"code_file" }                            # 仅用于 <对话树>.xmind
START_NODE_RESPONSE_SCRIPT_ATTRIBUTE               = { "cn":"应对话术", "en":"response_scripts" } 
START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_INTENT_FIELD            = { "cn":"意图", "en":"intent" }                           # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_INTENT_CONSTRAINT_FIELD = { "cn":"意图约束", "en":"intent_constraint" }                # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_RESPONSE_FIELD          = { "cn":"话术", "en":"response" }                         # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_FIXED_CLOSING_REMARKS_ATTRIBUTE         = { "cn":"固定结束语", "en":"fixed_closing_remarks" } 
START_NODE_STARTUP_INFOITEM_ATTRIBUTE                = { "cn":"启动信息项", "en":"startup_infoitem" }
START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE    = { "cn":"信息项变化触发", "en":"on_infoitem_change_trigger" } 
START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE_INFOITEM_FIELD = { "cn":"信息项", "en":"infoitem" }                 # 仅用于 python 对话树，xmind 对话树是其它表达方式
START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE_FUNCTION_FIELD = { "cn":"函数", "en":"function" }                   # 仅用于 python 对话树，xmind 对话树是其它表达方式, <对话树>.xmind 在内部是“函数名”string
START_NODE_LANGUAGE_STYLE_ATTRIBUTE                = { "cn":"语言风格", "en":"language_style" }
START_NODE_LANGUAGE_STYLE_ATTRIBUTE_NORMAL_VALUE          = { "cn":"正常", "en":"normal" }
START_NODE_LANGUAGE_STYLE_ATTRIBUTE_SPOKEN_VALUE          = { "cn":"口语", "en":"spoken" }
START_NODE_LANGUAGE_STYLE_ATTRIBUTE_WRITTEN_VALUE         = { "cn":"书面", "en":"written" }

TRIGGER_NODE      = { "cn":"触发", "en":"trigger" }

EMPTY_NODE        = { "cn":"空", "en":"empty" }

# for both SINGLE_TURN_NODE and MULTI_TURN_NODE 属性
INFOITEM_MODIFIER_ATTRIBUTE_IMPLICIT_VALUE               = { "cn":"隐含", "en":"implicit" } # SINGLE_TURN_NODE_INFOITEM_IMPLICIT_ATTRIBUTE   infoitem_implicit_attribute
INFOITEM_MODIFIER_ATTRIBUTE_EXPLICIT_VALUE               = { "cn":"明确", "en":"explicit" } # SINGLE_TURN_NODE_INFOITEM_EXPLICIT_ATTRIBUTE   infoitem_explicit_attribute
INFOITEM_MODIFIER_ATTRIBUTE_VERBATIM_VALUE               = { "cn":"原文", "en":"verbatim" } # SINGLE_TURN_NODE_INFOITEM_VERBATIM_ATTRIBUTE   infoitem_verbatim_attribute
INFOITEM_MODIFIER_ATTRIBUTE_REPEAT_VALUE                 = { "cn":"复述", "en":"repeat" }   # SINGLE_TURN_NODE_INFOITEM_REPEAT_ATTRIBUTE     infoitem_repeat_attribute
INFOITEM_MODIFIER_ATTRIBUTE_FIX_VALUE                    = { "cn":"固定", "en":"fixed" }    # SINGLE_TURN_NODE_INFOITEM_FIX_ATTRIBUTE        infoitem_fix_attribute
INFOITEM_MODIFIER_ATTRIBUTE_INCREMENTAL_VALUE            = { "cn":"增量", "en":"incremental" } # SINGLE_TURN_NODE_INFOITEM_INCREMENTAL_ATTRIBUTE  infoitem_incremental_attribute
INFOITEM_MODIFIER_ATTRIBUTE_REQUIRED_VALUE               = { "cn":"必需", "en":"required" }    # SINGLE_TURN_NODE_INFOITEM_REQUIRED_ATTRIBUTE     infoitem_required_attribute
INFOITEM_MODIFIER_ATTRIBUTE_TOPIC_VALUE                  = { "cn":"主题", "en":"topic" }       # SINGLE_TURN_NODE_INFOITEM_TOPIC_ATTRIBUTE        infoitem_topic_attribute
INFOITEM_MODIFIER_ATTRIBUTE_JSON_VALUE                   = { "cn":"json", "en":"json" }        # SINGLE_TURN_NODE_INFOITEM_JSON_ATTRIBUTE         infoitem_json_attribute
QUESTION_MODIFIER_ATTRIBUTE_REPHRASE_VALUE                 = { "cn":"改写", "en":"rephrase" }    # SINGLE_TURN_NODE_QUESTION_REPHRASE_ATTRIBUTE     question_rephrase_attribute
QUESTION_MODIFIER_ATTRIBUTE_ALWAYS_ASK_REPEATEDLY_VALUE    = { "cn":"总是反复提问", "en":"always_ask_repeatedly" } # SINGLE_TURN_NODE_QUESTION_ALWAYS_ASK_REPEATEDLY_ATTRIBUTE question_always_ask_repeatedly_attribute
QUESTION_MODIFIER_ATTRIBUTE_NEVER_ASK_REPEATEDLY_VALUE     = { "cn":"绝不反复提问", "en":"never_ask_repeatedly" }  # SINGLE_TURN_NODE_QUESTION_NEVER_ASK_REPEATEDLY_ATTRIBUTE  question_never_ask_repeatedly_attribute
INFOITEM_OPTIONS_MODIFIER_ATTRIBUTE_OPEN_ENDED_VALUE     = { "cn":"开放", "en":"open_ended" }
INFOITEM_OPTIONS_MODIFIER_ATTRIBUTE_MULTI_SELECT_VALUE   = { "cn":"多选", "en":"multi_select" }

SINGLE_TURN_NODE  = { "cn":"单次交互", "en":"single_turn_interact" }                                                   # 程序中简写为 single_turn 
SINGLE_TURN_NODE_INFOITEM_ATTRIBUTE                        = { "cn":"信息项", "en":"infoitem" }
SINGLE_TURN_NODE_INFOITEM_CONSTRAINT_ATTRIBUTE             = { "cn":"信息项约束", "en":"infoitem_constraint" }
SINGLE_TURN_NODE_INFOITEM_SYNONYM_ATTRIBUTE                = { "cn":"信息项同义词", "en":"infoitem_synonym" }
SINGLE_TURN_NODE_INFOITEM_MODIFIER_ATTRIBUTE               = { "cn":"信息项修饰", "en":"infoitem_modifier" }
SINGLE_TURN_NODE_INFOITEM_OPTIONS_ATTRIBUTE                = { "cn":"信息项选项", "en":"infoitem_options" }               # 仅 python 对话树有，而 xmind 对话树没有
SINGLE_TURN_NODE_INFOITEM_OPTIONS_MODIFIER_ATTRIBUTE       = { "cn":"信息项选项修饰", "en":"infoitem_options_modifier" }   # 仅 python 对话树有，而 xmind 对话树没有
SINGLE_TURN_NODE_EXECUTE_SCRIPT_BEFORE_ASKING_ATTRIBUTE    = { "cn":"提问前执行脚本", "en":"execute_script_before_asking" }      # 仅 python 对话树有，而 xmind 对话树没有
SINGLE_TURN_NODE_INFORM_USER_BEFORE_ASKING_ATTRIBUTE       = { "cn":"提问前提示用户", "en":"inform_user_before_asking" }         # 仅 python 对话树有，而 xmind 对话树没有
SINGLE_TURN_NODE_STRUCTURED_OUTPUT_BEFORE_ASKING_ATTRIBUTE = { "cn":"提问前结构化输出", "en":"structured_output_before_asking" } # 仅 python 对话树有，而 xmind 对话树没有
SINGLE_TURN_NODE_SYSTEM_QUESTION_ATTRIBUTE                 = { "cn":"系统问题", "en":"system_question" }                  # 程序内部是 system_question_text
SINGLE_TURN_NODE_QUESTION_MODIFIER_ATTRIBUTE               = { "cn":"问题修饰", "en":"question_modifier" }

USER_INTENT_NODE  = { "cn":"用户意图", "en":"user_intent" }
USER_INTENT_NODE_INTENT_ATTRIBUTE                     = { "cn":"意图", "en":"intent" }                                     # 仅用于 python 对话树，xmind 对话树是其它表达方式
USER_INTENT_NODE_INTENT_CONSTRAINT_ATTRIBUTE          = { "cn":"意图约束", "en":"intent_constraint" }                      # 仅用于 python 对话树，xmind 对话树是其它表达方式
USER_INTENT_NODE_RE_ENTER_INTENT_ATTRIBUTE            = { "cn":"重入意图", "en":"re_enter_intent" }                        # 仅用于 python 对话树，xmind 对话树是其它表达方式
USER_INTENT_NODE_RE_ENTER_INTENT_CONSTRAINT_ATTRIBUTE = { "cn":"重入意图约束", "en":"re_enter_intent_constraint" }         # 仅用于 python 对话树，xmind 对话树是其它表达方式

MULTI_TURN_NODE   = { "cn":"多轮交互", "en":"multi_turn_interact" }                                                      # 程序内部简写为 multi_turn
# MULTI_TURN_NODE_CHAT_TACTICS_ATTRIBUTE                 = { "cn":"对话策略", "en":"chat_tactics" }
MULTI_TURN_NODE_NOTIFY_NOT_FOUND_REF_INFO_ATTRIBUTE    = { "cn":"提示未找到参考信息", "en":"notify_not_found_reference_information" }
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE                 = { "cn":"抽取信息", "en":"extract_info" }
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_FIELD                  = { "cn":"信息项", "en":"infoitem" }                      # 仅用于 python 对话树，xmind 对话树是其它表达方式
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_CONSTRAINT_FIELD       = { "cn":"信息项约束", "en":"infoitem_constraint" }       # 仅用于 python 对话树，xmind 对话树是其它表达方式
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_MODIFIER_FIELD         = { "cn":"信息项修饰", "en":"infoitem_modifier" }         # 仅用于 python 对话树，xmind 对话树是其它表达方式
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_OPTIONS_FIELD          = { "cn":"信息项选项", "en":"infoitem_options" }               # 仅 python 对话树有，而 xmind 对话树没有
MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_OPTIONS_MODIFIER_FIELD = { "cn":"信息项选项修饰", "en":"infoitem_options_modifier" }   # 仅 python 对话树有，而 xmind 对话树没有
MULTI_TURN_NODE_EXECUTE_SCRIPT_ATTRIBUTE               = { "cn":"执行脚本", "en":"execute_script" }                      # 用于 xmind 对话树  (与下面互斥)
MULTI_TURN_NODE_EXECUTE_FUNCTION_ATTRIBUTE             = { "cn":"执行函数", "en":"execute_function" }                    # 用于 python 对话树 (与上面互斥)
MULTI_TURN_NODE_EXIT_INTENT_ATTRIBUTE                  = { "cn":"退出意图", "en":"exit_intent" }

ACTIVITY_NODE     = { "cn":"动作", "en":"activity" }

ACTIVITY_NODE_CALL_HTTP_SUBTYPE      = { "cn":"调用http", "en":"call_http" }
ACTIVITY_NODE_CALL_HTTP_SUBTYPE_URL_ATTRIBUTE = { "cn":"链接", "en":"url" }                     # 仅用于 python 对话树，xmind 对话树是其它表达方式

ACTIVITY_NODE_ASK_AGAIN_SUBTYPE      = { "cn":"重新提问", "en":"ask_again" }
ACTIVITY_NODE_ASK_AGAIN_SUBTYPE_INFOITEM_ATTRIBUTE = { "cn":"信息项", "en":"infoitem" }         # 仅用于 python 对话树，xmind 对话树是其它表达方式

ACTIVITY_NODE_SKIP_ASKING_SUBTYPE    = { "cn":"跳过提问", "en":"skip_asking" }
ACTIVITY_NODE_SKIP_ASKING_SUBTYPE_INFOITEM_ATTRIBUTE = { "cn":"信息项", "en":"infoitem" }       # 仅用于 python 对话树，xmind 对话树是其它表达方式

ACTIVITY_NODE_CLEAR_INFO_SUBTYPE     = { "cn":"清除信息", "en":"clear_info" }
ACTIVITY_NODE_CLEAR_INFO_SUBTYPE_INFOITEM_ATTRIBUTE = { "cn":"信息项", "en":"infoitem" }        # 仅用于 python 对话树，xmind 对话树是其它表达方式

ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE  = { "cn":"调用子树", "en":"call_sub_tree" }
ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE_SUB_TREE_ATTRIBUTE = { "cn":"子树", "en":"sub_tree" }       # 仅用于 python 对话树，xmind 对话树是其它表达方式

ACTIVITY_NODE_RETURN_SUBTYPE         = { "cn":"返回", "en":"return" }

ACTIVITY_NODE_TERMINATE_SUBTYPE      = { "cn":"结束", "en":"terminate" }

ACTIVITY_NODE_TRANSFER_HUMAN_AGENT_SUBTYPE = { "cn":"转接人工", "en":"transfer_human_agent" }

ACTIVITY_NODE_RESTART_SUBTYPE        = { "cn":"重启", "en":"restart" }

ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE = { "cn":"执行脚本", "en":"execute_script" }
ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE = { "cn":"函数", "en":"function" }       # 仅用于 python 对话树，xmind 对话树是其它表达方式, <对话树>.xmind 在内部是待执行的 script

CONDITION_NODE    = { "cn":"条件", "en":"condition" }

CONDITION_NODE_SCRIPT_SUBTYPE      = { "cn":"脚本", "en":"script" }
CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE = { "cn":"函数", "en":"function" }              # 仅用于 python 对话树，xmind 对话树是其它表达方式, <对话树>.xmind 在内部是待执行的 script

CONDITION_NODE_DESCRIPTION_SUBTYPE = { "cn":"描述", "en":"description" }
CONDITION_NODE_DESCRIPTION_SUBTYPE_DESCRIPTION_ATTRIBUTE = { "cn":"描述", "en":"description" }   # 仅用于 python 对话树，xmind 对话树是其它表达方式

INFORM_USER_NODE  = { "cn":"提示用户", "en":"inform_user" }
INFORM_USER_NODE_INFORM_CONTENT_ATTRIBUTE  = { "cn":"提示内容", "en":"inform_content" }           # 仅用于 python 对话树，xmind 对话树是其它表达方式

STRUCTURED_OUTPUT_NODE = { "cn":"结构化输出", "en":"structured_output" }
STRUCTURED_OUTPUT_NODE_OUTPUT_CONTENT_ATTRIBUTE = { "cn":"输出内容", "en":"output_content" }      # 仅用于 python 对话树，xmind 对话树是其它表达方式

#------------------------------------------------------------------------

SYSTEM_INFOITEM_CURR_USER_INPUT        = { "cn":"{_当前用户输入_}", "en":"{_curr_user_input_}" }
SYSTEM_INFOITEM_CURR_NODE_EXEC_COUNT   = { "cn":"{_当前节点正在执行的次数_}", "en":"{_curr_node_exec_count_}" }
SYSTEM_INFOITEM_PHONE_NUMBER           = { "cn":"{_电话号码_}", "en":"{_phone_number_}" }
SYSTEM_INFOITEM_TIME_ZONE              = { "cn":"{_时区_}", "en":"{_time_zone_}" }
SYSTEM_INFOITEM_USER_GENDER            = { "cn":"{_用户性别_}", "en":"{_user_gender_}" }
SYSTEM_INFOITEM_USER_AGE_GROUP         = { "cn":"{_用户年龄段_}", "en":"{_user_age_group_}" }
SYSTEM_INFOITEM_MULTI_TURN_EXIT_REASON = { "cn":"{_多轮交互退出原因_}", "en":"{_multi_turn_interact_exit_reason_}" }
SYSTEM_INFOITEM_MULTI_TURN_DYNA_PROMPT = { "cn":"{_多轮交互动态提示词_}", "en":"{_multi_turn_interact_dynamic_prompt_}" }
SYSTEM_INFOITEM_REF_INFO_HIT_CONTENT   = { "cn":"{_参考信息命中内容_}", "en":"{_reference_information_hit_content_}" } # 动态 + 静态
all_system_infoitem_list = [
    SYSTEM_INFOITEM_CURR_USER_INPUT[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_CURR_NODE_EXEC_COUNT[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_PHONE_NUMBER[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_TIME_ZONE[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_USER_GENDER[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_USER_AGE_GROUP[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_MULTI_TURN_EXIT_REASON[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_MULTI_TURN_DYNA_PROMPT[chattree_def_lang_sgn()],
    SYSTEM_INFOITEM_REF_INFO_HIT_CONTENT[chattree_def_lang_sgn()],
]

INFOITEM_STATE_UNKNOWN          = { "cn":"<未知>", "en":"<UNKNOWN>" }
INFOITEM_STATE_USER_NOT_PROVIDE = { "cn":"<用户未提供>", "en":"<USER_NOT_PROVIDE>" }
INFOITEM_STATE_ASSIGNED         = { "cn":"<已赋值>", "en":"<ASSIGNED>" }

#------------------------------------------------------------------------

StartNodeDynamicReferenceInformationElement = TypedDict(
    "StartNodeDynamicReferenceInformationElement",
    {
        START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_INTENT_FIELD[chattree_def_lang_sgn()]         : str,
        START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_INTENT_CONSTRAINT_FIELD[chattree_def_lang_sgn()] : NotRequired[str],
        START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE_FUNCTION_FIELD[chattree_def_lang_sgn()]       : Callable[[object], str],
    }
)
StartNodeIntentTriggerElement = TypedDict(
    "StartNodeIntentTriggerElement",
    {
        START_NODE_INTENT_TRIGGER_ATTRIBUTE_INTENT_FIELD[chattree_def_lang_sgn()]            : str,
        START_NODE_INTENT_TRIGGER_ATTRIBUTE_INTENT_CONSTRAINT_FIELD[chattree_def_lang_sgn()] : NotRequired[str],
        START_NODE_INTENT_TRIGGER_ATTRIBUTE_FUNCTION_FIELD[chattree_def_lang_sgn()]          : Callable[[object], str],
    }
)
StartNodeResponseScriptElement = TypedDict(
    "StartNodeResponseScriptElement",
    {
        START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_INTENT_FIELD[chattree_def_lang_sgn()]         : str,
        START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_INTENT_CONSTRAINT_FIELD[chattree_def_lang_sgn()] : NotRequired[str],
        START_NODE_RESPONSE_SCRIPT_ATTRIBUTE_RESPONSE_FIELD[chattree_def_lang_sgn()]       : str | list[str],   # here str could be ... or <...>
    }
)
StartNodeOnInfoitemChangeTriggerElement = TypedDict(
    "StartNodeOnInfoitemChangeTriggerElement",
    {
        START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE_INFOITEM_FIELD[chattree_def_lang_sgn()] : str | list[str],
        START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE_FUNCTION_FIELD[chattree_def_lang_sgn()] : Callable[[object], Any],
    }
)
StartNodeParams = TypedDict(
    "StartNodeParams",
    {
        START_NODE_CHATTREE_TITLE_ATTRIBUTE[chattree_def_lang_sgn()]                : NotRequired[str],
        START_NODE_SYSTEM_ROLE_ATTRIBUTE[chattree_def_lang_sgn()]                   : NotRequired[str],
        START_NODE_USER_ROLE_ATTRIBUTE[chattree_def_lang_sgn()]                     : NotRequired[str],
        START_NODE_BACKGROUND_INFORMATION_ATTRIBUTE[chattree_def_lang_sgn()]        : NotRequired[str],
        START_NODE_ALLOW_TRANSFER_HUMAN_AGENT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[bool],
        START_NODE_STATIC_REFERENCE_INFORMATION_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[str],
        START_NODE_DYNAMIC_REFERENCE_INFORMATION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[StartNodeDynamicReferenceInformationElement | list[StartNodeDynamicReferenceInformationElement]],
        START_NODE_INTENT_TRIGGER_ATTRIBUTE[chattree_def_lang_sgn()]                : NotRequired[StartNodeIntentTriggerElement | list[StartNodeIntentTriggerElement]],
        START_NODE_RESPONSE_SCRIPT_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[StartNodeResponseScriptElement | list[StartNodeResponseScriptElement]],
        START_NODE_FIXED_CLOSING_REMARKS_ATTRIBUTE[chattree_def_lang_sgn()]         : NotRequired[str],
        START_NODE_STARTUP_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()]                : NotRequired[str | list[str]],
        START_NODE_ON_INFOITEM_CHANGE_TRIGGER_ATTRIBUTE[chattree_def_lang_sgn()]    : NotRequired[StartNodeOnInfoitemChangeTriggerElement | list[StartNodeOnInfoitemChangeTriggerElement]],
        START_NODE_LANGUAGE_STYLE_ATTRIBUTE[chattree_def_lang_sgn()]                : NotRequired[str],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

TriggerNodeParams = TypedDict(
    "TriggerNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

EmptyNodeParams = TypedDict(
    "EmptyNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

SingleTurnInteractNodeParams = TypedDict(
    "SingleTurnInteractNodeParams",
    {
        SINGLE_TURN_NODE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()]                  : str,
        SINGLE_TURN_NODE_INFOITEM_CONSTRAINT_ATTRIBUTE[chattree_def_lang_sgn()]       : NotRequired[str],
        SINGLE_TURN_NODE_INFOITEM_SYNONYM_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[list[str]],
        SINGLE_TURN_NODE_INFOITEM_MODIFIER_ATTRIBUTE[chattree_def_lang_sgn()]         : NotRequired[list[str]],
        SINGLE_TURN_NODE_INFOITEM_OPTIONS_ATTRIBUTE[chattree_def_lang_sgn()]             : NotRequired[list[str]],
        SINGLE_TURN_NODE_INFOITEM_OPTIONS_MODIFIER_ATTRIBUTE[chattree_def_lang_sgn()]    : NotRequired[list[str]],
        SINGLE_TURN_NODE_EXECUTE_SCRIPT_BEFORE_ASKING_ATTRIBUTE[chattree_def_lang_sgn()]     : NotRequired[Callable[[object], Any]],
        SINGLE_TURN_NODE_INFORM_USER_BEFORE_ASKING_ATTRIBUTE[chattree_def_lang_sgn()]        : NotRequired[str | list[str]],
        SINGLE_TURN_NODE_STRUCTURED_OUTPUT_BEFORE_ASKING_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[str],
        SINGLE_TURN_NODE_SYSTEM_QUESTION_ATTRIBUTE[chattree_def_lang_sgn()]           : str | list[str],  # here str could be ... or <...>
        SINGLE_TURN_NODE_QUESTION_MODIFIER_ATTRIBUTE[chattree_def_lang_sgn()]         : NotRequired[list[str]],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

UserIntentNodeParams = TypedDict(
    "UserIntentNodeParams",
    {
        USER_INTENT_NODE_INTENT_ATTRIBUTE[chattree_def_lang_sgn()]                  : str,
        USER_INTENT_NODE_INTENT_CONSTRAINT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[str],
        USER_INTENT_NODE_RE_ENTER_INTENT_ATTRIBUTE[chattree_def_lang_sgn()]         : NotRequired[str],
        USER_INTENT_NODE_RE_ENTER_INTENT_CONSTRAINT_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[str],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

MultiTurnInteractExtractInfoElement = TypedDict(
    "MultiTurnInteractExtractInfoElement",
    {
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_FIELD[chattree_def_lang_sgn()]         : str,
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_CONSTRAINT_FIELD[chattree_def_lang_sgn()] : NotRequired[str],
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_MODIFIER_FIELD[chattree_def_lang_sgn()] : NotRequired[list[str]],
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_OPTIONS_FIELD[chattree_def_lang_sgn()]             : NotRequired[list[str]],
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE_INFOITEM_OPTIONS_MODIFIER_FIELD[chattree_def_lang_sgn()]    : NotRequired[list[str]],
    }
)
MultiTurnInteractNodeParams = TypedDict(
    "MultiTurnInteractNodeParams",
    {
        # MULTI_TURN_NODE_CHAT_TACTICS_ATTRIBUTE[chattree_def_lang_sgn()]     : NotRequired[str],
        MULTI_TURN_NODE_NOTIFY_NOT_FOUND_REF_INFO_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[str],
        MULTI_TURN_NODE_EXTRACT_INFO_ATTRIBUTE[chattree_def_lang_sgn()]     : NotRequired[MultiTurnInteractExtractInfoElement | list[MultiTurnInteractExtractInfoElement]],
        MULTI_TURN_NODE_EXECUTE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[Callable[[object], Any]],
        MULTI_TURN_NODE_EXIT_INTENT_ATTRIBUTE[chattree_def_lang_sgn()]      : NotRequired[str | list[str]],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]      : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]       : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[str],
    }
)

CallHttpActivityNodeParams = TypedDict(
    "CallHttpActivityNodeParams",
    {
        ACTIVITY_NODE_CALL_HTTP_SUBTYPE_URL_ATTRIBUTE[chattree_def_lang_sgn()] : str,
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]         : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                  : NotRequired[str],
    }
)

AskAgainActivityNodeParams = TypedDict(
    "AskAgainActivityNodeParams",
    {
        ACTIVITY_NODE_ASK_AGAIN_SUBTYPE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()] : str | list[str],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],

    }
)

SkipAskingActivityNodeParams = TypedDict(
    "SkipAskingActivityNodeParams",
    {
        ACTIVITY_NODE_SKIP_ASKING_SUBTYPE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()] : str | list[str],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

ClearInfoActivityNodeParams = TypedDict(
    "ClearInfoActivityNodeParams",
    {
        ACTIVITY_NODE_CLEAR_INFO_SUBTYPE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()] : str | list[str],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],

    }
)

CallSubTreeActivityNodeParams = TypedDict(
    "CallSubTreeActivityNodeParams",
    {
        ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE_SUB_TREE_ATTRIBUTE[chattree_def_lang_sgn()] : str,
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

ReturnActivityNodeParams = TypedDict(
    "ReturnActivityNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[str],
    }
)

TerminateActivityNodeParams = TypedDict(
    "TerminateActivityNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[str],
    }
)

TransferHumanActivityNodeParams = TypedDict(
    "TransferHumanActivityNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[str],
    }
)

RestartActivityNodeParams = TypedDict(
    "RestartActivityNodeParams",
    {
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()] : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]  : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]          : NotRequired[str],
    }
)

ExecuteScriptActivityNodeParams = TypedDict(
    "ExecuteScriptActivityNodeParams",
    {
        ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()] : Callable[[object], Any],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

ScriptConditionNodeParams = TypedDict(
    "ScriptConditionNodeParams",
    {
        CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()] : Callable[[object], bool],
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]           : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]            : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                    : NotRequired[str],
    }
)

DescriptionConditionNodeParams = TypedDict(
    "DescriptionConditionNodeParams",
    {
        CONDITION_NODE_DESCRIPTION_SUBTYPE_DESCRIPTION_ATTRIBUTE[chattree_def_lang_sgn()] : str,
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]               : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                       : NotRequired[str],
    }
)

InformUserNodeParams = TypedDict(
    "InformUserNodeParams",
    {
        INFORM_USER_NODE_INFORM_CONTENT_ATTRIBUTE[chattree_def_lang_sgn()] : str | list[str],  # here str could be ... or <...>
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]     : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]      : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]              : NotRequired[str],
    }
)

StructuredOutputNodeParams = TypedDict(
    "StructuredOutputNodeParams",
    {
        STRUCTURED_OUTPUT_NODE_OUTPUT_CONTENT_ATTRIBUTE[chattree_def_lang_sgn()] : str,
        ALL_NODE_BEFORE_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]           : NotRequired[Callable[[object], Any]],
        ALL_NODE_AFTER_EXECUTION_ATTRIBUTE[chattree_def_lang_sgn()]            : NotRequired[Callable[[object], Any]],
        ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]                    : NotRequired[str],
    }
)

# ------------------------------------------------------------------------


def get_tail(obj): # 辅助函数，用于获取一个对象的"尾部"
    if isinstance(obj, Pipeline):
        return obj.tail
    return obj # 如果是 Node, tail就是它自己
class Pipeline:
    def __init__(self, head, tail):
        self.head = head # 链的起始 (用于被上游连接)
        self.tail = tail # 链的末尾 (用于连接下游)，可以是单个Node，也可以是list of Nodes
    def __rshift__(self, other):
        # 让 pipeline 的所有尾部连接到 other
        tails = self.tail if isinstance(self.tail, list) else [self.tail]
        for t in tails:
            t >> other
        ###### 让兄弟对齐
        #####if len(tails) > 1:
        #####    _s = ""
        #####    for t in tails:
        #####        _s += ('"' + t.id + '"; ')
        #####    other.chattree.dot.body.append('{ rank=same; ' + _s + '}')
        # 链式传递：创建一个新的、更长的 pipeline
        # 新的 head 是旧的 head，新的 tail 是 other 的 tail
        return Pipeline(self.head, get_tail(other))
class NodeList(list): # 增强列表：处理 [A, B] >> C
    def __rshift__(self, other):
        for item in self:
            item >> other # 这里的 item 可能是 Node，也可能是 Pipeline
        if isinstance(other, list):
            return NodeList(other)
        return other
    
class Node:
    def __init__(self, chattree, full_node_type, var_name, node_params_class, params): # , group_obj):
        self.id = str(id(self))
        self.chattree = chattree
        self.full_node_type = full_node_type.strip()
        self.childrens = []
        self.parents = []
        self.var_name = var_name.strip()
        self.params = params

        self.chattree.node_list.append(self)

        # 多的key，少的key（除了NotRequired），类型错误的key，都报错
        try:
            check_type(params, node_params_class)
        except TypeCheckError as e:
            _json_str = json.dumps(params, indent=4, separators=(',', ':'), ensure_ascii=False, default=str)
            raise TypeError(
                chattree_def_lang_str(
                    cn = f"创建 {full_node_type} 节点时，参数“params”（见下文）错误",
                    en = f"Parameter 'params' (see below) is error  when create {full_node_type} node"
                ) +
                f" --->\n  {str(e)}\n" +
                _json_str
            )

        params_text = chattree_def_compact_json_dumps(self.params).strip()
        if params_text == "{}":
            params_text = ""
        self.full_caption = (
            # "="*25 + "\n" + self.full_node_type + "\n" + "="*25 + "\n" +
            # "(" + 
            # chattree_def_lang_str(en="var name is", cn="变量名为") +
            # " : " + self.var_name + ")" +
            "="*len(self.var_name) + "\n" + self.full_node_type + "\n" + self.var_name + "\n" + "="*len(self.var_name) + "\n\n" + params_text 
        ).strip()

        # self.caption = "="*chattree_def_display_len(self.full_node_type) + "\n" + self.full_node_type + "\n" + "="*chattree_def_display_len(self.full_node_type) + "\n"
        self.caption = self.full_node_type
        if ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()] in self.params and self.params[ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]]:
            # self.caption = self.caption + "\n" + (
            #     chattree_def_lang_str(en="(NodeComment: ", cn="（节点注释：") +
            #     self.params[ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]] +
            #     chattree_def_lang_str(en=")", cn="）")
            # )
            xlabel = self.params[ALL_NODE_COMMENT_ATTRIBUTE[chattree_def_lang_sgn()]].strip()
        else:
            xlabel = None

        if self.full_node_type in ["#"+START_NODE[chattree_def_lang_sgn()]+"#", "#"+TRIGGER_NODE[chattree_def_lang_sgn()]+"#"]:
            shape = "box"
            peripheries = "1"
            penwidth = "6" # "1"
            fillcolor = "white" # "black"
            color = "black" # fillcolor
            fontcolor = "black" # "white"
            margin = "0.2,0.2"
        elif self.full_node_type in ["#"+EMPTY_NODE[chattree_def_lang_sgn()]+"#"]:
            shape = "circle"
            peripheries = "1"
            penwidth = "1"
            fillcolor = "white"
            color = "black"
            fontcolor = "black"
            margin = "0.2,0.2"
        elif self.full_node_type in ["#"+INFORM_USER_NODE[chattree_def_lang_sgn()]+"#", "#"+STRUCTURED_OUTPUT_NODE[chattree_def_lang_sgn()]+"#"]:
            self.caption = self.caption + "\n" + (
                self.params[INFORM_USER_NODE_INFORM_CONTENT_ATTRIBUTE[chattree_def_lang_sgn()]]
                if self.full_node_type == "#"+INFORM_USER_NODE[chattree_def_lang_sgn()]+"#" else
                self.params[STRUCTURED_OUTPUT_NODE_OUTPUT_CONTENT_ATTRIBUTE[chattree_def_lang_sgn()]]
            )
            shape = "box"
            peripheries = "1"
            penwidth = "6" # "1"
            fillcolor = "white" # "#FF9595"
            color = "#FF9595" # fillcolor
            fontcolor = "black"
            margin = "0.2,0.2"
        elif self.full_node_type in ["#"+SINGLE_TURN_NODE[chattree_def_lang_sgn()]+"#", "#"+MULTI_TURN_NODE[chattree_def_lang_sgn()]+"#"]:
            self.caption = self.caption + "\n" + (
                self.params[SINGLE_TURN_NODE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()]] + "\n" + str(self.params[SINGLE_TURN_NODE_SYSTEM_QUESTION_ATTRIBUTE[chattree_def_lang_sgn()]])
                if self.full_node_type == "#"+SINGLE_TURN_NODE[chattree_def_lang_sgn()]+"#" else
                ""
            )
            shape = "box"
            peripheries = "1" # "6"
            penwidth = "1"
            fillcolor = "#CE4289"
            color = fillcolor # "#000000"
            fontcolor = "white"
            margin = "0.2,0.2"
        elif self.full_node_type in ["#"+USER_INTENT_NODE[chattree_def_lang_sgn()]+"#"]:
            self.caption = self.caption + "\n" + self.params[USER_INTENT_NODE_INTENT_ATTRIBUTE[chattree_def_lang_sgn()]]
            shape = "box"
            peripheries = "1"
            penwidth = "6" # "1"
            fillcolor = "white" # "#50C3F7"
            color = "#50C3F7" # fillcolor
            fontcolor = "black"
            margin = "0.2,0.2"
        elif self.full_node_type in ["#"+CONDITION_NODE[chattree_def_lang_sgn()]+"#"+CONDITION_NODE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()], "#"+CONDITION_NODE[chattree_def_lang_sgn()]+"#"+CONDITION_NODE_DESCRIPTION_SUBTYPE[chattree_def_lang_sgn()]]:
            self.caption = self.caption + "\n" + (
                self.params[CONDITION_NODE_DESCRIPTION_SUBTYPE_DESCRIPTION_ATTRIBUTE[chattree_def_lang_sgn()]]
                if self.full_node_type == "#"+CONDITION_NODE[chattree_def_lang_sgn()]+"#"+CONDITION_NODE_DESCRIPTION_SUBTYPE[chattree_def_lang_sgn()] else
                (
                    self.params[CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]].__name__ + "()"
                    if self.params[CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]].__name__ != "<lambda>" else
                    (
                        inspect.getsource(self.params[CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]]).strip()[
                            inspect.getsource(self.params[CONDITION_NODE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]]).strip().find("lambda"):
                        ]
                    )
                )
            )
            shape = "hexagon"
            peripheries = "1"
            penwidth = "6" # "1"
            fillcolor = "white" # "#FDD834"
            color = "#AD8E05" # fillcolor
            fontcolor = "black"
            margin = "0.2,0.0"
        elif self.full_node_type in [
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_SKIP_ASKING_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_CLEAR_INFO_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_ASK_AGAIN_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_RETURN_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_TERMINATE_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_RESTART_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_TRANSFER_HUMAN_AGENT_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_CALL_HTTP_SUBTYPE[chattree_def_lang_sgn()], 
            "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()]
        ]:
            if self.full_node_type == "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_ASK_AGAIN_SUBTYPE[chattree_def_lang_sgn()]:
                self.caption = self.caption + "\n" + self.params[ACTIVITY_NODE_ASK_AGAIN_SUBTYPE_INFOITEM_ATTRIBUTE[chattree_def_lang_sgn()]]
            elif self.full_node_type == "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE[chattree_def_lang_sgn()]:
                self.caption = self.caption + "\n" + self.params[ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE_SUB_TREE_ATTRIBUTE[chattree_def_lang_sgn()]]
            elif self.full_node_type == "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_CALL_HTTP_SUBTYPE[chattree_def_lang_sgn()]:
                self.caption = self.caption + "\n" + self.params[ACTIVITY_NODE_CALL_HTTP_SUBTYPE_URL_ATTRIBUTE[chattree_def_lang_sgn()]]
            elif self.full_node_type == "#"+ACTIVITY_NODE[chattree_def_lang_sgn()]+"#"+ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()]:
                self.caption = self.caption + "\n" + (
                    self.params[ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]].__name__ + "()"
                    if self.params[ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]].__name__ != "<lambda>" else
                    (
                        inspect.getsource(self.params[ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]]).strip()[
                            inspect.getsource(self.params[ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE_FUNCTION_ATTRIBUTE[chattree_def_lang_sgn()]]).strip().find("lambda"):
                        ]
                    )
                )
            shape = "box"
            peripheries = "1"
            penwidth = "6" # "1"
            fillcolor = "white" # "#8EDE99"
            color = "#70AD78" # fillcolor
            fontcolor = "black"
            margin = "0.2,0.2"
        else:
            raise Exception("Should not happen!")
        self.caption = self.caption.strip()
        line_list = self.caption.split("\n")
        truncate_line = 8
        if len(line_list) > truncate_line:
            line_list = line_list[:truncate_line]
            line_list.append("...")
        truncate_len = 60 # 这里一个汉字长度为 2，一个 ASCII 字符长度为 1
        for line_no in range(len(line_list)):
            _curr_line_real_len = 0
            _curr_line_cut_len = 0
            for _c in line_list[line_no]:
                if ord(_c) <= 127:
                    _curr_line_cut_len += 1
                    _curr_line_real_len += 1
                else:
                    _curr_line_cut_len += 2
                    _curr_line_real_len += 1
                if _curr_line_cut_len > truncate_len:
                    break
            if _curr_line_real_len < len(line_list[line_no]):
                line_list[line_no] = line_list[line_no][:_curr_line_real_len] + " ..."
            # if len(line_list[line_no]) > truncate_len:
            #     line_list[line_no] = line_list[line_no][:truncate_len] + " ..."
        self.caption = "\n".join(line_list)

        self.chattree.dot.attr('node', fontname='Courier New') # , fontname=chattree_def_lang_str(en="Courier New", cn="SimSun, Songti SC, serif")) # "Courier New, SimSun, Songti SC, serif" 英 'DejaVu Sans Mono'/'Courier New',  中 'Noto Sans CJK SC'(个别 linux 版本可能是 'Noto Sans SC')
        #--------------------
        # label   = self.caption.strip() # .replace("\n","\\l") + "\\l" # 左对齐
        raw_lines = self.caption.strip().split('\n')
        table_rows = []
        for index, line in enumerate(raw_lines):
            safe_line = html.escape(line)
            if not safe_line:
                safe_line = " "
            if index == 0:
                if chattree_def_lang_sgn() == "en":
                    content = f'<B><FONT POINT-SIZE="20" FACE="Arial">{safe_line}</FONT></B>'
                else:
                    content = f'<B><FONT POINT-SIZE="20">{safe_line}</FONT></B>'
            else:
                content = f'<FONT POINT-SIZE="14">{safe_line}</FONT>'
            # row_html = f'<TR><TD ALIGN="LEFT" BALIGN="LEFT" BORDER="0">{content}</TD></TR>'
            row_html = f'<TR><TD ALIGN="CENTER" BALIGN="CENTER" BORDER="0">{content}</TD></TR>'
            table_rows.append(row_html)
            if index == 0 and len(raw_lines) > 1:
                table_rows.append('<TR><TD BORDER="0" FIXEDSIZE="TRUE" WIDTH="10" HEIGHT="7"></TD></TR>')
        all_rows = "".join(table_rows)
        label = f'''<
            <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="2">
                {all_rows}
            </TABLE>
        >'''
        #---------------------
        tooltip = self.full_caption.strip() # .replace("\n\"","\n==>\"") # .replace("\n","\\l") + "\\l", # 左对齐
        # if group_obj is None:
        _obj = self.chattree.dot
        # else:
        #     _obj = group_obj
        if xlabel:
            xlabel = xlabel.replace("\n","&nbsp;&nbsp;<BR/>")
        _obj.node(
            name  = str(id(self)),
            label = label,
            xlabel = (
                f'< <FONT COLOR="{color}" POINT-SIZE="10">{xlabel}</FONT> >'
                if xlabel else
                None
            ),
            tooltip = tooltip,
            style = 'filled, rounded', # 'filled, rounded', # rounded / filled / dashed / dotted / invis
            fontsize  = '14', # '10',
            shape = shape, # box / ellipse / circle / diamond
            peripheries = peripheries,
            penwidth = penwidth,
            fillcolor = fillcolor,
            color = color,
            fontcolor = fontcolor,
            margin=margin,
        )

    def __rshift__(self, other):
        if self is other:
            raise Exception(
                chattree_def_lang_str(
                    cn = f"节点不能指向自身 : ",
                    en = f"A node can not point to itself : "
                ) + 
                self.full_caption.strip()
            )
        # 情况 1: Node >> [A, B]
        if isinstance(other, list): 
            # 这里的 other 可能是普通的 list，也可能是包含 Pipeline 的 list
            # 我们将其转换为 NodeList 以支持后续操作
            for item in other:
                self >> item # 递归调用，self 会连接到 item 的 head

            # =======================================================
            # [新增] 自动对齐逻辑：让列表里的节点水平对齐
            # =======================================================
            # 提取需要对齐的节点对象（如果是Pipeline，则取其head）
            nodes_to_align = []
            for item in other:
                if isinstance(item, Node):
                    nodes_to_align.append(item)
                elif isinstance(item, Pipeline):
                    nodes_to_align.append(item.head)
            
            # 只有当分支数量 > 1 时才需要对齐
            if len(nodes_to_align) > 1:
                # 构造符合 DOT 语法的 ID 列表，例如: "12345"; "67890";
                ids_str = "; ".join([f'"{n.id}"' for n in nodes_to_align])
                # 直接追加到 dot.body
                self.chattree.dot.body.append(f'{{ rank=same; {ids_str} }}')
            # =======================================================

            new_tails = [get_tail(item) for item in other]
            # 如果只有一个尾部，就不要用列表包装
            final_tail = new_tails[0] if len(new_tails) == 1 else new_tails
            return Pipeline(self, final_tail) # head 是自己 (self)，tail 是列表中所有元素的 tail 集合
        # 情况 2: Node >> Pipeline (即 A >> (B>>C))
        elif isinstance(other, Pipeline):
            # 我(A) 应该连接到对方的头(B)
            self >> other.head
            # 返回一个新的 Pipeline，头是我(A)，尾是对方的尾(C)
            return Pipeline(self, other.tail)
        # 情况 3: Node >> Node (即 A >> B)
        elif isinstance(other, Node):
            # self.chattree.dot.edge(self.id, other.id)
            self.chattree.dot.edge(f'{self.id}:e', f'{other.id}:w')
            self.childrens.append(other)
            other.parents.append(self)
            # 【关键】返回 Pipeline 而不是 other
            # 这样既保留了 A (head) 供上游使用(如果有)，也保留了 B (tail) 供下游使用
            return Pipeline(self, other)
        # 如果 other 是 NodeList, 视为 list 处理
        elif isinstance(other, NodeList):
            return self >> list(other)
        else:
            return other

class ChatTree:

    def __init__(self):
        self.dot = graphviz.Digraph()
        self.dot.attr(
            rankdir='LR',       # 核心：左到右布局
            # ordering="out",     # 按照执行顺序输出
            splines='polyline',   # 核心：使用折线（直角线） polyline curved line spline
            nodesep='0.6',      # 增加同级节点上下的间距，防止线条重叠
            ranksep='1.5',      # 增加层级之间的左右间距
            # overlap='false',    # 防止节点重叠
            # concentrate='true', # 合并多条边为一条
            # dpi = '150',
            layout='dot',       # 确保使用 dot 引擎（默认就是，但显式指定更安全）
            fontsize='14',      # 【新增】设置更大的默认字号
            ordering='out',     # 子节点的显示顺序
            
            # compound='true',
            # newrank='true',
        )
        self.dot.attr('edge',
            arrowhead='vee',    # 箭头样式
            arrowsize='2.0',    # 箭头大小
            penwidth='1.5',
        )
        # self.dot.attr('node', 
        #     margin='0.15'      # 给文字周围留点白
        # )
        self.node_list = []
        # self.group = self.dot.subgraph

    def render(self, filename): # filename = __file__
        if not filename.lower().endswith('.html'):
            if filename.lower().endswith('.py'):
                filename = filename[:-3] + '.html'
            else:
                filename = filename + '.html'
        svg_bytes = self.dot.pipe(format='svg')
        svg_content = svg_bytes.decode('utf-8') 
        
        # 注意：在 Python f-string 中，CSS和JS的大括号 {} 需要写成 double {{ }}
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>Interactive Graph</title>
            <style>
                html, body {{
                    width: 100%;
                    height: 100%;
                    margin: 0;
                    padding: 0;
                    overflow: hidden;
                    background-color: #f0f0f0;
                    font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
                }}
                #container {{
                    width: 100%;
                    height: 100%;
                    border: 1px solid #ccc;
                    position: relative;
                }}
                svg {{
                    width: 100%;
                    height: 100%;
                }}
                
                /* --- 搜索框样式 --- */
                #search-container {{
                    position: absolute;
                    top: 20px;
                    left: 20px;
                    z-index: 100;
                    background: rgba(255, 255, 255, 0.95);
                    padding: 12px;
                    border-radius: 6px;
                    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
                    display: flex;
                    gap: 8px;
                }}
                #search-input {{
                    padding: 6px 10px;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    width: 200px;
                    outline: none;
                }}
                #search-input:focus {{ border-color: #007bff; }}
                #search-btn {{
                    padding: 6px 12px;
                    cursor: pointer;
                    background-color: #007bff;
                    color: white;
                    border: none;
                    border-radius: 4px;
                    font-weight: bold;
                }}
                #search-btn:hover {{ background-color: #0056b3; }}

                /* --- 自定义 Tooltip 样式 (解决截断问题) --- */
                #custom-tooltip {{
                    position: absolute;
                    display: none;
                    z-index: 9999;
                    background: rgba(50, 50, 50, 0.95);
                    color: #fff;
                    border: 1px solid #333;
                    padding: 6px 10px;       /* 内边距调小，更紧凑 */
                    border-radius: 6px;
                    box-shadow: 4px 4px 15px rgba(0,0,0,0.3);
                    
                    /* 1. 宽度修改：变宽 */
                    max-width: 900px;        /* 限制最大宽度，也可以改成 80vw (屏幕宽度的80%) */
                    min-width: 100px;
                    white-space: pre-wrap;   
                    word-wrap: break-word;   
                    
                    font-family: "Consolas", "Monaco", monospace;
                    
                    /* 2. 字体修改：变小 */
                    font-size: 10px;         /* 字体调小 */
                    line-height: 1.2;       /* 行高调小，节省纵向空间 */
                    pointer-events: none;    
                }}

                #copy-toast {{
                    position: absolute;
                    display: none;
                    z-index: 10000;
                    left: 20px;
                    bottom: 20px;
                    background: rgba(0, 0, 0, 0.78);
                    color: #fff;
                    border-radius: 6px;
                    padding: 8px 12px;
                    font-size: 12px;
                    max-width: 700px;
                    white-space: pre-wrap;
                    word-break: break-word;
                }}

                /* --- 搜索高亮样式 --- */
                .highlight-node polygon, 
                .highlight-node ellipse, 
                .highlight-node path {{
                    stroke: #ff0000 !important;
                    stroke-width: 4px !important;
                    fill: #fff3cd !important;
                }}
                .highlight-text text {{
                    fill: #d60000 !important;
                    font-weight: bold !important;
                    font-size: 1.1em !important;
                }}
            </style>
            <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
        </head>
        <body>
            <div id="container">
                <!-- 搜索栏 -->
                <div id="search-container">
                    <input type="text" id="search-input" placeholder="Search content...">
                    <button id="search-btn" onclick="searchGraph()">Find</button>
                </div>
                
                <!-- 自定义 Tooltip 容器 -->
                <div id="custom-tooltip"></div>
                <div id="copy-toast"></div>

                {svg_content}
            </div>
            
            <script>
                var panZoomInstance; 

                window.onload = function() {{
                    var svgElement = document.querySelector('svg');
                    
                    if (svgElement) {{
                        if (!svgElement.id) {{ svgElement.id = 'main-svg'; }}
                        
                        // 1. 初始化缩放
                        if (typeof svgPanZoom === 'function') {{
                            try {{
                                panZoomInstance = svgPanZoom('#' + svgElement.id, {{
                                    zoomEnabled: true,
                                    controlIconsEnabled: false,
                                    fit: true,
                                    center: true,
                                    minZoom: 0.1,
                                    maxZoom: 100,
                                }});
                            }} catch (e) {{
                                console.warn('svgPanZoom init failed, fallback to basic pan/zoom', e);
                                enableBasicPanZoom(svgElement);
                            }}
                        }} else {{
                            console.warn('svgPanZoom not found, fallback to basic pan/zoom');
                            enableBasicPanZoom(svgElement);
                        }}

                        // 2. 初始化 Tooltip 并清洗原生 title
                        initTooltips(svgElement);

                        // 3. 绑定回车搜索
                        document.getElementById('search-input').addEventListener('keypress', function (e) {{
                            if (e.key === 'Enter') {{ searchGraph(); }}
                        }});

                    }} else {{
                        console.error("SVG element not found");
                    }}
                }};

                function initTooltips(svg) {{
                    var tooltip = document.getElementById('custom-tooltip');
                    var nodes = svg.querySelectorAll('.node');

                    // --- 第一步：先从节点中把数据提取出来，存到 data-tooltip 中 ---
                    nodes.forEach(function(node) {{
                        var finalTooltipText = "";

                        // 1. 尝试找 <title> 标签内容 (ID)
                        var titleNode = node.querySelector('title');
                        if (titleNode && titleNode.textContent.trim()) {{
                            finalTooltipText = titleNode.textContent.trim();
                        }}

                        // 2. 尝试找子元素的 title 属性 (真正的 tooltip 内容)
                        // 我们遍历节点内所有子元素，谁有 title 就用谁的
                        var children = node.querySelectorAll('*');
                        children.forEach(function(child) {{
                            var attrTitle = child.getAttribute('title') || child.getAttribute('xlink:title');
                            if (attrTitle) {{
                                finalTooltipText = attrTitle;
                            }}
                        }});
                        
                        // 3. 检查节点自身
                        var selfTitle = node.getAttribute('title') || node.getAttribute('xlink:title');
                        if (selfTitle) finalTooltipText = selfTitle;

                        // 绑定事件
                        if (finalTooltipText) {{
                            node.setAttribute('data-tooltip', finalTooltipText);
                            
                            // 绑定鼠标事件
                            node.addEventListener('mouseenter', function(e) {{
                                var text = this.getAttribute('data-tooltip');
                                if(text) {{
                                    tooltip.innerText = text; 
                                    tooltip.style.display = 'block';
                                }}
                            }});
                            
                            node.addEventListener('mousemove', function(e) {{
                                var x = e.clientX + 20; 
                                var y = e.clientY + 20;
                                // 边界检测
                                if (x + tooltip.offsetWidth > window.innerWidth) {{
                                    x = e.clientX - tooltip.offsetWidth - 10;
                                }}
                                if (y + tooltip.offsetHeight > window.innerHeight) {{
                                    y = e.clientY - tooltip.offsetHeight - 10;
                                }}
                                tooltip.style.left = x + 'px';
                                tooltip.style.top = y + 'px';
                            }});

                            node.addEventListener('mouseleave', function() {{
                                tooltip.style.display = 'none';
                            }});

                            node.addEventListener('click', function(e) {{
                                if (e.button !== 0) return; // 仅左键
                                handleNodeLeftClick(this);
                            }});
                            node.addEventListener('mouseup', function(e) {{
                                if (e.button !== 0) return; // 仅左键
                                handleNodeLeftClick(this);
                            }});
                        }}
                    }});

                    // --- 第二步：核弹级清理 ---
                    // 不再依赖特定选择器，直接遍历 SVG 下的所有元素
                    var allElements = svg.querySelectorAll('*');
                    
                    allElements.forEach(function(el) {{
                        // 1. 如果是 <title> 标签，直接删除节点
                        if (el.tagName.toLowerCase() === 'title') {{
                            el.remove();
                        }} else {{
                            // 2. 如果是其他标签，移除所有可能触发弹窗的属性
                            el.removeAttribute('title');
                            el.removeAttribute('xlink:title');
                            // 针对带有命名空间的属性进行强制移除
                            el.removeAttributeNS('http://www.w3.org/1999/xlink', 'title');
                        }}
                    }});
                    
                    // 别忘了把 SVG 根节点自己的 title 也删了
                    svg.removeAttribute('title');
                }}

                function copyTextToClipboard(text) {{
                    return new Promise(function(resolve) {{
                        try {{
                            if (navigator.clipboard && window.isSecureContext) {{
                                navigator.clipboard.writeText(text).then(function() {{
                                    resolve(true);
                                }}).catch(function() {{
                                    resolve(copyTextByExecCommand(text));
                                }});
                                return;
                            }}
                        }} catch (e) {{}}
                        resolve(copyTextByExecCommand(text));
                    }});
                }}

                function copyTextByExecCommand(text) {{
                    try {{
                        var ta = document.createElement('textarea');
                        ta.value = text;
                        ta.style.position = 'fixed';
                        ta.style.left = '-9999px';
                        document.body.appendChild(ta);
                        ta.focus();
                        ta.select();
                        var ok = document.execCommand('copy');
                        document.body.removeChild(ta);
                        return ok;
                    }} catch (e) {{
                        return false;
                    }}
                }}

                function handleNodeLeftClick(node) {{
                    var now = Date.now();
                    var lastTs = Number(node.getAttribute('data-last-copy-ts') || 0);
                    if (now - lastTs < 250) return;
                    node.setAttribute('data-last-copy-ts', String(now));

                    var text = node.getAttribute('data-tooltip') || '';
                    var lines = text.split(/\\r?\\n/);
                    var line3 = (lines.length >= 3 ? lines[2] : '').trim();
                    if (!line3) {{
                        showToast('Line 3 does not exist or is empty and cannot be copied');
                        return;
                    }}
                    copyTextToClipboard(line3).then(function(ok) {{
                        showToast(ok ? 'Node variable name has been copied to clipboard : ' + line3 : 'Copy failed (browser blocked clipboard)');
                    }});
                }}

                function showToast(text) {{
                    var toast = document.getElementById('copy-toast');
                    if (!toast) return;
                    toast.textContent = text;
                    toast.style.display = 'block';
                    clearTimeout(showToast._timer);
                    showToast._timer = setTimeout(function() {{
                        toast.style.display = 'none';
                    }}, 3000);
                }}

                function enableBasicPanZoom(svgElement) {{
                    var vb = svgElement.getAttribute('viewBox');
                    if (!vb) return;
                    var parts = vb.split(/\\s+/).map(Number);
                    if (parts.length !== 4 || parts.some(isNaN)) return;

                    var state = {{ x: parts[0], y: parts[1], w: parts[2], h: parts[3], dragging: false, lastX: 0, lastY: 0 }};
                    function applyViewBox() {{
                        svgElement.setAttribute('viewBox', [state.x, state.y, state.w, state.h].join(' '));
                    }}

                    svgElement.addEventListener('wheel', function(e) {{
                        e.preventDefault();
                        var zoomIn = e.deltaY < 0;
                        var factor = zoomIn ? 0.9 : 1.1;
                        var rect = svgElement.getBoundingClientRect();
                        var rx = (e.clientX - rect.left) / rect.width;
                        var ry = (e.clientY - rect.top) / rect.height;
                        var nw = state.w * factor;
                        var nh = state.h * factor;
                        state.x += (state.w - nw) * rx;
                        state.y += (state.h - nh) * ry;
                        state.w = nw;
                        state.h = nh;
                        applyViewBox();
                    }}, {{ passive: false }});

                    svgElement.addEventListener('mousedown', function(e) {{
                        if (e.button !== 0) return;
                        state.dragging = true;
                        state.lastX = e.clientX;
                        state.lastY = e.clientY;
                    }});
                    window.addEventListener('mousemove', function(e) {{
                        if (!state.dragging) return;
                        var rect = svgElement.getBoundingClientRect();
                        var dx = (e.clientX - state.lastX) * (state.w / rect.width);
                        var dy = (e.clientY - state.lastY) * (state.h / rect.height);
                        state.x -= dx;
                        state.y -= dy;
                        state.lastX = e.clientX;
                        state.lastY = e.clientY;
                        applyViewBox();
                    }});
                    window.addEventListener('mouseup', function() {{
                        state.dragging = false;
                    }});
                }}

                function searchGraph() {{
                    var input = document.getElementById('search-input');
                    var query = input.value.trim().toLowerCase();
                    
                    // 清除旧的高亮
                    document.querySelectorAll('.highlight-node').forEach(n => {{
                        n.classList.remove('highlight-node');
                        n.classList.remove('highlight-text');
                    }});

                    if (!query) return;

                    var allNodes = document.querySelectorAll('.node');
                    var found = false;

                    allNodes.forEach(function(node) {{
                        // 同时搜索显示的 label 和隐藏的 tooltip 内容
                        var nodeText = node.textContent.toLowerCase();
                        var tooltipText = (node.getAttribute('data-tooltip') || "").toLowerCase();

                        if (nodeText.includes(query) || tooltipText.includes(query)) {{
                            node.classList.add('highlight-node');
                            node.classList.add('highlight-text');
                            found = true;
                        }}
                    }});

                    if (!found) {{
                        // 简单的闪烁提示表示没找到
                        input.style.backgroundColor = "#ffe6e6";
                        setTimeout(() => input.style.backgroundColor = "white", 500);
                    }}
                }}
            </script>
        </body>
        </html>
        """

        with open(filename, 'w') as f:
            f.write(html_content)
        print("Output file:", os.path.abspath(filename))

    def create_node(self, full_node_type:str, params:dict={}): # , group_obj=None):
        try:
            var_name = varname()
        except Exception:
            var_name = ""

        full_node_type = full_node_type.lower().strip()
        if re.match(r"^#[\u4e00-\u9fa50-9a-zA-Z_\-]+$", full_node_type):
            full_node_type = full_node_type + "#" # 加上最后的“#”
        if re.match(r"^#[\u4e00-\u9fa50-9a-zA-Z_\-]+#[\u4e00-\u9fa50-9a-zA-Z_\-]+#$", full_node_type):
            full_node_type = full_node_type[:-1]  # 去掉最后的“#”

        if full_node_type.lower().strip() not in [
            "#" + START_NODE[chattree_def_lang_sgn()] + "#",
            "#" + TRIGGER_NODE[chattree_def_lang_sgn()] + "#",
            "#" + EMPTY_NODE[chattree_def_lang_sgn()] + "#",
            "#" + SINGLE_TURN_NODE[chattree_def_lang_sgn()] + "#",
            "#" + USER_INTENT_NODE[chattree_def_lang_sgn()] + "#",
            "#" + MULTI_TURN_NODE[chattree_def_lang_sgn()] + "#",
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_HTTP_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_ASK_AGAIN_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_SKIP_ASKING_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CLEAR_INFO_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RETURN_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TERMINATE_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TRANSFER_HUMAN_AGENT_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RESTART_SUBTYPE[chattree_def_lang_sgn()],
            "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()],
            "#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()],
            "#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_DESCRIPTION_SUBTYPE[chattree_def_lang_sgn()],
            "#" + INFORM_USER_NODE[chattree_def_lang_sgn()] + "#",
            "#" + STRUCTURED_OUTPUT_NODE[chattree_def_lang_sgn()] + "#",
        ]:
            raise Exception(
                chattree_def_lang_str(
                    cn = "未知的节点类型 : “" + full_node_type + "”，应为下列预定义的节点类型之一：",
                    en = "Unknown full_node_type : \"" + full_node_type + "\", should be one of the predefined node types: "
                ) +
                f"#" + START_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + TRIGGER_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + EMPTY_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + SINGLE_TURN_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + USER_INTENT_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + MULTI_TURN_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_HTTP_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_ASK_AGAIN_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_SKIP_ASKING_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CLEAR_INFO_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RETURN_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TERMINATE_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TRANSFER_HUMAN_AGENT_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RESTART_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_DESCRIPTION_SUBTYPE[chattree_def_lang_sgn()] + ", " +
                f"#" + INFORM_USER_NODE[chattree_def_lang_sgn()] + "#, " +
                f"#" + STRUCTURED_OUTPUT_NODE[chattree_def_lang_sgn()] + "#."
            )

        if full_node_type == "#" + START_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = StartNodeParams
        elif full_node_type == "#" + TRIGGER_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = TriggerNodeParams
        elif full_node_type == "#" + EMPTY_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = EmptyNodeParams
        elif full_node_type == "#" + SINGLE_TURN_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = SingleTurnInteractNodeParams
        elif full_node_type == "#" + USER_INTENT_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = UserIntentNodeParams
        elif full_node_type == "#" + MULTI_TURN_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = MultiTurnInteractNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_HTTP_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = CallHttpActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_ASK_AGAIN_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = AskAgainActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_SKIP_ASKING_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = SkipAskingActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CLEAR_INFO_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = ClearInfoActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_CALL_SUB_TREE_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = CallSubTreeActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RETURN_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = ReturnActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TERMINATE_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = TerminateActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_TRANSFER_HUMAN_AGENT_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = TransferHumanActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_RESTART_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = RestartActivityNodeParams
        elif full_node_type == "#" + ACTIVITY_NODE[chattree_def_lang_sgn()] + "#" + ACTIVITY_NODE_EXECUTE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = ExecuteScriptActivityNodeParams
        elif full_node_type == "#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_SCRIPT_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = ScriptConditionNodeParams
        elif full_node_type == "#" + CONDITION_NODE[chattree_def_lang_sgn()] + "#" + CONDITION_NODE_DESCRIPTION_SUBTYPE[chattree_def_lang_sgn()]:
            node_params_class = DescriptionConditionNodeParams
        elif full_node_type == "#" + INFORM_USER_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = InformUserNodeParams
        elif full_node_type == "#" + STRUCTURED_OUTPUT_NODE[chattree_def_lang_sgn()] + "#":
            node_params_class = StructuredOutputNodeParams
        else:
            raise Exception("Should not happen!")
        
        # if group_obj is not None:
        #     if not group_obj.name:
        #         group_obj.name = "cluster_" + str(id(group_obj))
        #     else:
        #         if not group_obj.name.startswith("cluster"):
        #             group_obj.name = "cluster_" + group_obj.name
        #     group_obj.attr(style='filled', color='grey') # 设置分组的背景色
        #     group_obj.attr(label=group_obj.name)  # 分组的标题
        #     group_obj.attr(rankdir="LR")

        node = Node(
            self,
            full_node_type = full_node_type,
            var_name = var_name,
            node_params_class = node_params_class,
            params = node_params_class(params),
            # group_obj = group_obj,
        )
        return node
