mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-08-09 12:18:12 +00:00
Merge branch 'feat/sprint-flow-remaining-requirements' into feat/sprint-flow2.0
This commit is contained in:
commit
21146bb40f
@ -1,3 +1,6 @@
|
|||||||
|
.env
|
||||||
|
.git/
|
||||||
|
./.mypy_cache/
|
||||||
models/
|
models/
|
||||||
plugins/
|
plugins/
|
||||||
pilot/data
|
pilot/data
|
||||||
@ -5,6 +8,8 @@ pilot/message
|
|||||||
logs/
|
logs/
|
||||||
venv/
|
venv/
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
|
web/.next/
|
||||||
|
web/.env
|
||||||
docs/node_modules/
|
docs/node_modules/
|
||||||
build/
|
build/
|
||||||
docs/build/
|
docs/build/
|
||||||
|
@ -332,6 +332,10 @@ class Config(metaclass=Singleton):
|
|||||||
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
|
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.SCHEDULER_ENABLED = (
|
||||||
|
os.getenv("SCHEDULER_ENABLED", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_db_manager(self) -> "ConnectorManager":
|
def local_db_manager(self) -> "ConnectorManager":
|
||||||
from dbgpt.datasource.manages import ConnectorManager
|
from dbgpt.datasource.manages import ConnectorManager
|
||||||
|
@ -19,12 +19,14 @@ class DefaultScheduler(BaseComponent):
|
|||||||
system_app: SystemApp,
|
system_app: SystemApp,
|
||||||
scheduler_delay_ms: int = 5000,
|
scheduler_delay_ms: int = 5000,
|
||||||
scheduler_interval_ms: int = 1000,
|
scheduler_interval_ms: int = 1000,
|
||||||
|
scheduler_enable: bool = True,
|
||||||
):
|
):
|
||||||
super().__init__(system_app)
|
super().__init__(system_app)
|
||||||
self.system_app = system_app
|
self.system_app = system_app
|
||||||
self._scheduler_interval_ms = scheduler_interval_ms
|
self._scheduler_interval_ms = scheduler_interval_ms
|
||||||
self._scheduler_delay_ms = scheduler_delay_ms
|
self._scheduler_delay_ms = scheduler_delay_ms
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
self._scheduler_enable = scheduler_enable
|
||||||
|
|
||||||
def init_app(self, system_app: SystemApp):
|
def init_app(self, system_app: SystemApp):
|
||||||
self.system_app = system_app
|
self.system_app = system_app
|
||||||
@ -39,7 +41,7 @@ class DefaultScheduler(BaseComponent):
|
|||||||
|
|
||||||
def _scheduler(self):
|
def _scheduler(self):
|
||||||
time.sleep(self._scheduler_delay_ms / 1000)
|
time.sleep(self._scheduler_delay_ms / 1000)
|
||||||
while not self._stop_event.is_set():
|
while self._scheduler_enable and not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
schedule.run_pending()
|
schedule.run_pending()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -145,6 +145,9 @@ class DAGVar:
|
|||||||
_executor: Optional[Executor] = None
|
_executor: Optional[Executor] = None
|
||||||
|
|
||||||
_variables_provider: Optional["VariablesProvider"] = None
|
_variables_provider: Optional["VariablesProvider"] = None
|
||||||
|
# Whether check serializable for AWEL, it will be set to True when running AWEL
|
||||||
|
# operator in remote environment
|
||||||
|
_check_serializable: Optional[bool] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enter_dag(cls, dag) -> None:
|
def enter_dag(cls, dag) -> None:
|
||||||
@ -257,6 +260,24 @@ class DAGVar:
|
|||||||
"""
|
"""
|
||||||
cls._variables_provider = variables_provider
|
cls._variables_provider = variables_provider
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_check_serializable(cls) -> Optional[bool]:
|
||||||
|
"""Get the check serializable flag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[bool]: The check serializable flag
|
||||||
|
"""
|
||||||
|
return cls._check_serializable
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_check_serializable(cls, check_serializable: bool) -> None:
|
||||||
|
"""Set the check serializable flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
check_serializable (bool): The check serializable flag to set
|
||||||
|
"""
|
||||||
|
cls._check_serializable = check_serializable
|
||||||
|
|
||||||
|
|
||||||
class DAGLifecycle:
|
class DAGLifecycle:
|
||||||
"""The lifecycle of DAG."""
|
"""The lifecycle of DAG."""
|
||||||
@ -286,6 +307,7 @@ class DAGNode(DAGLifecycle, DependencyMixin, ViewMixin, ABC):
|
|||||||
node_name: Optional[str] = None,
|
node_name: Optional[str] = None,
|
||||||
system_app: Optional[SystemApp] = None,
|
system_app: Optional[SystemApp] = None,
|
||||||
executor: Optional[Executor] = None,
|
executor: Optional[Executor] = None,
|
||||||
|
check_serializable: Optional[bool] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a DAGNode.
|
"""Initialize a DAGNode.
|
||||||
@ -311,6 +333,7 @@ class DAGNode(DAGLifecycle, DependencyMixin, ViewMixin, ABC):
|
|||||||
node_id = self._dag._new_node_id()
|
node_id = self._dag._new_node_id()
|
||||||
self._node_id: Optional[str] = node_id
|
self._node_id: Optional[str] = node_id
|
||||||
self._node_name: Optional[str] = node_name
|
self._node_name: Optional[str] = node_name
|
||||||
|
self._check_serializable = check_serializable
|
||||||
if self._dag:
|
if self._dag:
|
||||||
self._dag._append_node(self)
|
self._dag._append_node(self)
|
||||||
|
|
||||||
@ -486,6 +509,20 @@ class DAGNode(DAGLifecycle, DependencyMixin, ViewMixin, ABC):
|
|||||||
"""Return the string of current DAGNode."""
|
"""Return the string of current DAGNode."""
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _do_check_serializable(cls, obj: Any, obj_name: str = "Object"):
|
||||||
|
"""Check whether the current DAGNode is serializable."""
|
||||||
|
from dbgpt.util.serialization.check import check_serializable
|
||||||
|
|
||||||
|
check_serializable(obj, obj_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def check_serializable(self) -> bool:
|
||||||
|
"""Whether check serializable for current DAGNode."""
|
||||||
|
if self._check_serializable is not None:
|
||||||
|
return self._check_serializable or False
|
||||||
|
return DAGVar.get_check_serializable() or False
|
||||||
|
|
||||||
|
|
||||||
def _build_task_key(task_name: str, key: str) -> str:
|
def _build_task_key(task_name: str, key: str) -> str:
|
||||||
return f"{task_name}___$$$$$$___{key}"
|
return f"{task_name}___$$$$$$___{key}"
|
||||||
|
@ -193,12 +193,29 @@ class BaseOperator(DAGNode, ABC, Generic[OUT], metaclass=BaseOperatorMeta):
|
|||||||
self.incremental_output = bool(kwargs["incremental_output"])
|
self.incremental_output = bool(kwargs["incremental_output"])
|
||||||
if "output_format" in kwargs:
|
if "output_format" in kwargs:
|
||||||
self.output_format = kwargs["output_format"]
|
self.output_format = kwargs["output_format"]
|
||||||
|
|
||||||
self._runner: WorkflowRunner = runner
|
self._runner: WorkflowRunner = runner
|
||||||
self._dag_ctx: Optional[DAGContext] = None
|
self._dag_ctx: Optional[DAGContext] = None
|
||||||
self._can_skip_in_branch = can_skip_in_branch
|
self._can_skip_in_branch = can_skip_in_branch
|
||||||
self._variables_provider = variables_provider
|
self._variables_provider = variables_provider
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
"""Customize the pickling process."""
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
if "_runner" in state:
|
||||||
|
del state["_runner"]
|
||||||
|
if "_executor" in state:
|
||||||
|
del state["_executor"]
|
||||||
|
if "_system_app" in state:
|
||||||
|
del state["_system_app"]
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
"""Customize the unpickling process."""
|
||||||
|
self.__dict__.update(state)
|
||||||
|
self._runner = default_runner
|
||||||
|
self._system_app = DAGVar.get_current_system_app()
|
||||||
|
self._executor = DAGVar.get_executor()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_dag_context(self) -> DAGContext:
|
def current_dag_context(self) -> DAGContext:
|
||||||
"""Return the current DAG context."""
|
"""Return the current DAG context."""
|
||||||
|
@ -41,6 +41,12 @@ class JoinOperator(BaseOperator, Generic[OUT]):
|
|||||||
super().__init__(can_skip_in_branch=can_skip_in_branch, **kwargs)
|
super().__init__(can_skip_in_branch=can_skip_in_branch, **kwargs)
|
||||||
if not callable(combine_function):
|
if not callable(combine_function):
|
||||||
raise ValueError("combine_function must be callable")
|
raise ValueError("combine_function must be callable")
|
||||||
|
|
||||||
|
if self.check_serializable:
|
||||||
|
super()._do_check_serializable(
|
||||||
|
combine_function,
|
||||||
|
f"JoinOperator: {self}, combine_function: {combine_function}",
|
||||||
|
)
|
||||||
self.combine_function = combine_function
|
self.combine_function = combine_function
|
||||||
|
|
||||||
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
||||||
@ -83,6 +89,11 @@ class ReduceStreamOperator(BaseOperator, Generic[IN, OUT]):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
if reduce_function and not callable(reduce_function):
|
if reduce_function and not callable(reduce_function):
|
||||||
raise ValueError("reduce_function must be callable")
|
raise ValueError("reduce_function must be callable")
|
||||||
|
if reduce_function and self.check_serializable:
|
||||||
|
super()._do_check_serializable(
|
||||||
|
reduce_function, f"Operator: {self}, reduce_function: {reduce_function}"
|
||||||
|
)
|
||||||
|
|
||||||
self.reduce_function = reduce_function
|
self.reduce_function = reduce_function
|
||||||
|
|
||||||
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
||||||
@ -133,6 +144,12 @@ class MapOperator(BaseOperator, Generic[IN, OUT]):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
if map_function and not callable(map_function):
|
if map_function and not callable(map_function):
|
||||||
raise ValueError("map_function must be callable")
|
raise ValueError("map_function must be callable")
|
||||||
|
|
||||||
|
if map_function and self.check_serializable:
|
||||||
|
super()._do_check_serializable(
|
||||||
|
map_function, f"Operator: {self}, map_function: {map_function}"
|
||||||
|
)
|
||||||
|
|
||||||
self.map_function = map_function
|
self.map_function = map_function
|
||||||
|
|
||||||
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
async def _do_run(self, dag_ctx: DAGContext) -> TaskOutput[OUT]:
|
||||||
|
@ -94,6 +94,17 @@ class ProxyLLMClient(LLMClient):
|
|||||||
self.executor = executor or ThreadPoolExecutor()
|
self.executor = executor or ThreadPoolExecutor()
|
||||||
self.proxy_tokenizer = proxy_tokenizer or TiktokenProxyTokenizer()
|
self.proxy_tokenizer = proxy_tokenizer or TiktokenProxyTokenizer()
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
"""Customize the serialization of the object"""
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
state.pop("executor")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
"""Customize the deserialization of the object"""
|
||||||
|
self.__dict__.update(state)
|
||||||
|
self.executor = ThreadPoolExecutor()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def new_client(
|
def new_client(
|
||||||
|
@ -341,7 +341,7 @@ class BuiltinAgentsVariablesProvider(BuiltinVariablesProvider):
|
|||||||
StorageVariables(
|
StorageVariables(
|
||||||
key=key,
|
key=key,
|
||||||
name=agent["name"],
|
name=agent["name"],
|
||||||
label=agent["desc"],
|
label=agent["name"],
|
||||||
value=agent["name"],
|
value=agent["name"],
|
||||||
scope=scope,
|
scope=scope,
|
||||||
scope_key=scope_key,
|
scope_key=scope_key,
|
||||||
|
@ -285,6 +285,9 @@ class BaseDao(Generic[T, REQ, RES]):
|
|||||||
else model_to_dict(query_request)
|
else model_to_dict(query_request)
|
||||||
)
|
)
|
||||||
for key, value in query_dict.items():
|
for key, value in query_dict.items():
|
||||||
|
if value and isinstance(value, (list, tuple, dict, set)):
|
||||||
|
# Skip the list, tuple, dict, set
|
||||||
|
continue
|
||||||
if value is not None and hasattr(model_cls, key):
|
if value is not None and hasattr(model_cls, key):
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
if len(value) > 0:
|
if len(value) > 0:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import errno
|
import errno
|
||||||
import socket
|
import socket
|
||||||
|
from typing import Set, Tuple
|
||||||
|
|
||||||
|
|
||||||
def _get_ip_address(address: str = "10.254.254.254:1") -> str:
|
def _get_ip_address(address: str = "10.254.254.254:1") -> str:
|
||||||
@ -22,3 +23,34 @@ def _get_ip_address(address: str = "10.254.254.254:1") -> str:
|
|||||||
finally:
|
finally:
|
||||||
s.close()
|
s.close()
|
||||||
return curr_address
|
return curr_address
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_free_port(
|
||||||
|
port_range: Tuple[int, int], timeout: int, used_ports: Set[int]
|
||||||
|
):
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
return await loop.run_in_executor(
|
||||||
|
None, _get_free_port, port_range, timeout, used_ports
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_free_port(port_range: Tuple[int, int], timeout: int, used_ports: Set[int]):
|
||||||
|
import random
|
||||||
|
|
||||||
|
available_ports = set(range(port_range[0], port_range[1] + 1)) - used_ports
|
||||||
|
if not available_ports:
|
||||||
|
raise RuntimeError("No available ports in the specified range")
|
||||||
|
|
||||||
|
while available_ports:
|
||||||
|
port = random.choice(list(available_ports))
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("", port))
|
||||||
|
used_ports.add(port)
|
||||||
|
return port
|
||||||
|
except OSError:
|
||||||
|
available_ports.remove(port)
|
||||||
|
|
||||||
|
raise RuntimeError("No available ports in the specified range")
|
||||||
|
85
dbgpt/util/serialization/check.py
Normal file
85
dbgpt/util/serialization/check.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import inspect
|
||||||
|
from io import StringIO
|
||||||
|
from typing import Any, Dict, Optional, TextIO
|
||||||
|
|
||||||
|
import cloudpickle
|
||||||
|
|
||||||
|
|
||||||
|
def check_serializable(
|
||||||
|
obj: Any, obj_name: str = "Object", error_msg: str = "Object is not serializable"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
cloudpickle.dumps(obj)
|
||||||
|
except Exception as e:
|
||||||
|
inspect_info = inspect_serializability(obj, obj_name)
|
||||||
|
msg = f"{error_msg}\n{inspect_info['report']}"
|
||||||
|
raise TypeError(msg) from e
|
||||||
|
|
||||||
|
|
||||||
|
class SerializabilityInspector:
|
||||||
|
def __init__(self, stream: Optional[TextIO] = None):
|
||||||
|
self.stream = stream or StringIO()
|
||||||
|
self.failures = {}
|
||||||
|
self.indent_level = 0
|
||||||
|
|
||||||
|
def log(self, message: str):
|
||||||
|
indent = " " * self.indent_level
|
||||||
|
self.stream.write(f"{indent}{message}\n")
|
||||||
|
|
||||||
|
def inspect(self, obj: Any, name: str, depth: int = 3) -> bool:
|
||||||
|
self.log(f"Inspecting '{name}'")
|
||||||
|
self.indent_level += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
cloudpickle.dumps(obj)
|
||||||
|
self.indent_level -= 1
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.failures[name] = str(e)
|
||||||
|
self.log(f"Failure: {str(e)}")
|
||||||
|
|
||||||
|
if depth > 0:
|
||||||
|
if inspect.isfunction(obj) or inspect.ismethod(obj):
|
||||||
|
self._inspect_function(obj, depth - 1)
|
||||||
|
elif hasattr(obj, "__dict__"):
|
||||||
|
self._inspect_object(obj, depth - 1)
|
||||||
|
|
||||||
|
self.indent_level -= 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _inspect_function(self, func, depth):
|
||||||
|
closure = inspect.getclosurevars(func)
|
||||||
|
for name, value in closure.nonlocals.items():
|
||||||
|
self.inspect(value, f"{func.__name__}.{name}", depth)
|
||||||
|
for name, value in closure.globals.items():
|
||||||
|
self.inspect(value, f"global:{name}", depth)
|
||||||
|
|
||||||
|
def _inspect_object(self, obj, depth):
|
||||||
|
for name, value in inspect.getmembers(obj):
|
||||||
|
if not name.startswith("__"):
|
||||||
|
self.inspect(value, f"{type(obj).__name__}.{name}", depth)
|
||||||
|
|
||||||
|
def get_report(self) -> str:
|
||||||
|
summary = "\nSummary of Serialization Failures:\n"
|
||||||
|
if not self.failures:
|
||||||
|
summary += "All components are serializable.\n"
|
||||||
|
else:
|
||||||
|
for name, error in self.failures.items():
|
||||||
|
summary += f" - {name}: {error}\n"
|
||||||
|
|
||||||
|
return self.stream.getvalue() + summary
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_serializability(
|
||||||
|
obj: Any,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
depth: int = 5,
|
||||||
|
stream: Optional[TextIO] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
inspector = SerializabilityInspector(stream)
|
||||||
|
success = inspector.inspect(obj, name or type(obj).__name__, depth)
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"failures": inspector.failures,
|
||||||
|
"report": inspector.get_report(),
|
||||||
|
}
|
@ -20,16 +20,21 @@ LOAD_EXAMPLES="true"
|
|||||||
BUILD_NETWORK=""
|
BUILD_NETWORK=""
|
||||||
DB_GPT_INSTALL_MODEL="default"
|
DB_GPT_INSTALL_MODEL="default"
|
||||||
|
|
||||||
|
DOCKERFILE="Dockerfile"
|
||||||
|
IMAGE_NAME_SUFFIX=""
|
||||||
|
|
||||||
usage () {
|
usage () {
|
||||||
echo "USAGE: $0 [--base-image nvidia/cuda:12.1.0-runtime-ubuntu22.04] [--image-name db-gpt]"
|
echo "USAGE: $0 [--base-image nvidia/cuda:12.1.0-runtime-ubuntu22.04] [--image-name db-gpt]"
|
||||||
echo " [-b|--base-image base image name] Base image name"
|
echo " [-b|--base-image base image name] Base image name"
|
||||||
echo " [-n|--image-name image name] Current image name, default: db-gpt"
|
echo " [-n|--image-name image name] Current image name, default: db-gpt"
|
||||||
|
echo " [--image-name-suffix image name suffix] Image name suffix"
|
||||||
echo " [-i|--pip-index-url pip index url] Pip index url, default: https://pypi.org/simple"
|
echo " [-i|--pip-index-url pip index url] Pip index url, default: https://pypi.org/simple"
|
||||||
echo " [--language en or zh] You language, default: en"
|
echo " [--language en or zh] You language, default: en"
|
||||||
echo " [--build-local-code true or false] Whether to use the local project code to package the image, default: true"
|
echo " [--build-local-code true or false] Whether to use the local project code to package the image, default: true"
|
||||||
echo " [--load-examples true or false] Whether to load examples to default database default: true"
|
echo " [--load-examples true or false] Whether to load examples to default database default: true"
|
||||||
echo " [--network network name] The network of docker build"
|
echo " [--network network name] The network of docker build"
|
||||||
echo " [--install-mode mode name] Installation mode name, default: default, If you completely use openai's service, you can set the mode name to 'openai'"
|
echo " [--install-mode mode name] Installation mode name, default: default, If you completely use openai's service, you can set the mode name to 'openai'"
|
||||||
|
echo " [-f|--dockerfile dockerfile] Dockerfile name, default: Dockerfile"
|
||||||
echo " [-h|--help] Usage message"
|
echo " [-h|--help] Usage message"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +51,11 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift # past argument
|
shift # past argument
|
||||||
shift # past value
|
shift # past value
|
||||||
;;
|
;;
|
||||||
|
--image-name-suffix)
|
||||||
|
IMAGE_NAME_SUFFIX="$2"
|
||||||
|
shift # past argument
|
||||||
|
shift # past value
|
||||||
|
;;
|
||||||
-i|--pip-index-url)
|
-i|--pip-index-url)
|
||||||
PIP_INDEX_URL="$2"
|
PIP_INDEX_URL="$2"
|
||||||
shift
|
shift
|
||||||
@ -80,6 +90,11 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift # past argument
|
shift # past argument
|
||||||
shift # past value
|
shift # past value
|
||||||
;;
|
;;
|
||||||
|
-f|--dockerfile)
|
||||||
|
DOCKERFILE="$2"
|
||||||
|
shift # past argument
|
||||||
|
shift # past value
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
help="true"
|
help="true"
|
||||||
shift
|
shift
|
||||||
@ -111,6 +126,10 @@ else
|
|||||||
BASE_IMAGE=$IMAGE_NAME_ARGS
|
BASE_IMAGE=$IMAGE_NAME_ARGS
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "$IMAGE_NAME_SUFFIX" ]; then
|
||||||
|
IMAGE_NAME="$IMAGE_NAME-$IMAGE_NAME_SUFFIX"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Begin build docker image, base image: ${BASE_IMAGE}, target image name: ${IMAGE_NAME}"
|
echo "Begin build docker image, base image: ${BASE_IMAGE}, target image name: ${IMAGE_NAME}"
|
||||||
|
|
||||||
docker build $BUILD_NETWORK \
|
docker build $BUILD_NETWORK \
|
||||||
@ -120,5 +139,5 @@ docker build $BUILD_NETWORK \
|
|||||||
--build-arg BUILD_LOCAL_CODE=$BUILD_LOCAL_CODE \
|
--build-arg BUILD_LOCAL_CODE=$BUILD_LOCAL_CODE \
|
||||||
--build-arg LOAD_EXAMPLES=$LOAD_EXAMPLES \
|
--build-arg LOAD_EXAMPLES=$LOAD_EXAMPLES \
|
||||||
--build-arg DB_GPT_INSTALL_MODEL=$DB_GPT_INSTALL_MODEL \
|
--build-arg DB_GPT_INSTALL_MODEL=$DB_GPT_INSTALL_MODEL \
|
||||||
-f Dockerfile \
|
-f $DOCKERFILE \
|
||||||
-t $IMAGE_NAME $WORK_DIR/../../
|
-t $IMAGE_NAME $WORK_DIR/../../
|
||||||
|
@ -6,6 +6,10 @@ import {
|
|||||||
IFlowRefreshParams,
|
IFlowRefreshParams,
|
||||||
IFlowResponse,
|
IFlowResponse,
|
||||||
IFlowUpdateParam,
|
IFlowUpdateParam,
|
||||||
|
IGetKeysRequestParams,
|
||||||
|
IGetKeysResponseData,
|
||||||
|
IGetVariablesByKeyRequestParams,
|
||||||
|
IGetVariablesByKeyResponseData,
|
||||||
IUploadFileRequestParams,
|
IUploadFileRequestParams,
|
||||||
IUploadFileResponse,
|
IUploadFileResponse,
|
||||||
} from '@/types/flow';
|
} from '@/types/flow';
|
||||||
@ -63,17 +67,22 @@ export const downloadFile = (fileId: string) => {
|
|||||||
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
|
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO:wait for interface update
|
|
||||||
export const getFlowTemplateList = () => {
|
|
||||||
return GET<null, Array<any>>('/api/v2/serve/awel/flow/templates');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFlowTemplateById = (id: string) => {
|
export const getFlowTemplateById = (id: string) => {
|
||||||
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
|
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlowTemplates = () => {
|
export const getFlowTemplates = () => {
|
||||||
return GET<null, any>(`/api/v2/serve/awel/flow/templates`);
|
return GET<null, any>(`/api/v2/serve/awel/flow/templates`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getKeys = (data?: IGetKeysRequestParams) => {
|
||||||
|
return GET<IGetKeysRequestParams, Array<IGetKeysResponseData>>('/api/v2/serve/awel/variables/keys', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getVariablesByKey = (data: IGetVariablesByKeyRequestParams) => {
|
||||||
|
return GET<IGetVariablesByKeyRequestParams, IGetVariablesByKeyResponseData>('/api/v2/serve/awel/variables', data);
|
||||||
|
};
|
||||||
|
|
||||||
export const metadataBatch = (data: IUploadFileRequestParams) => {
|
export const metadataBatch = (data: IUploadFileRequestParams) => {
|
||||||
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/metadata/batch', data);
|
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/metadata/batch', data);
|
||||||
};
|
};
|
@ -4,7 +4,8 @@ import { IFlowNode } from '@/types/flow';
|
|||||||
import { FLOW_NODES_KEY } from '@/utils';
|
import { FLOW_NODES_KEY } from '@/utils';
|
||||||
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
|
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||||
import type { CollapseProps } from 'antd';
|
import type { CollapseProps } from 'antd';
|
||||||
import { Badge, Collapse, Input, Layout, Space, Tag } from 'antd';
|
import { Badge, Collapse, Input, Layout, Space, Switch } from 'antd';
|
||||||
|
import classnames from 'classnames';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import StaticNodes from './static-nodes';
|
import StaticNodes from './static-nodes';
|
||||||
@ -199,9 +200,13 @@ const AddNodesSider: React.FC = () => {
|
|||||||
{t('add_node')}
|
{t('add_node')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Tag color={isAllNodesVisible ? 'green' : 'blue'} onClick={onModeChange} className='mr-0'>
|
<Switch
|
||||||
{isAllNodesVisible ? t('All_Nodes') : t('Higher_Order_Nodes')}
|
checkedChildren='高阶'
|
||||||
</Tag>
|
unCheckedChildren='全部'
|
||||||
|
onClick={onModeChange}
|
||||||
|
className={classnames('w-20', { 'bg-zinc-400': isAllNodesVisible })}
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Search placeholder='Search node' onSearch={searchNode} allowClear />
|
<Search placeholder='Search node' onSearch={searchNode} allowClear />
|
||||||
|
@ -1,70 +1,58 @@
|
|||||||
// import { IFlowNode } from '@/types/flow';
|
import { apiInterceptors, getKeys, getVariablesByKey } from '@/client/api';
|
||||||
|
import { IFlowUpdateParam, IGetKeysResponseData, IVariableItem } from '@/types/flow';
|
||||||
|
import { buildVariableString } from '@/utils/flow';
|
||||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Form, Input, Modal, Select, Space } from 'antd';
|
import { Button, Cascader, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import { DefaultOptionType } from 'antd/es/cascader';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// ype GroupType = { category: string; categoryLabel: string; nodes: IFlowNode[] };
|
|
||||||
type ValueType = 'str' | 'int' | 'float' | 'bool' | 'ref';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
const VALUE_TYPES = ['str', 'int', 'float', 'bool', 'ref'] as const;
|
||||||
|
|
||||||
const DAG_PARAM_KEY = 'dbgpt.core.flow.params';
|
type ValueType = (typeof VALUE_TYPES)[number];
|
||||||
const DAG_PARAM_SCOPE = 'flow_priv';
|
type Props = {
|
||||||
|
flowInfo?: IFlowUpdateParam;
|
||||||
|
setFlowInfo: React.Dispatch<React.SetStateAction<IFlowUpdateParam | undefined>>;
|
||||||
|
};
|
||||||
|
|
||||||
export const AddFlowVariableModal: React.FC = () => {
|
export const AddFlowVariableModal: React.FC<Props> = ({ flowInfo, setFlowInfo }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// const [operators, setOperators] = useState<Array<IFlowNode>>([]);
|
|
||||||
// const [resources, setResources] = useState<Array<IFlowNode>>([]);
|
|
||||||
// const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
|
|
||||||
// const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [form] = Form.useForm(); // const [form] = Form.useForm<IFlowUpdateParam>();
|
const [form] = Form.useForm();
|
||||||
|
const [controlTypes, setControlTypes] = useState<ValueType[]>(['str']);
|
||||||
|
const [refVariableOptions, setRefVariableOptions] = useState<DefaultOptionType[]>([]);
|
||||||
|
|
||||||
const showModal = () => {
|
useEffect(() => {
|
||||||
setIsModalOpen(true);
|
getKeysData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getKeysData = async () => {
|
||||||
|
const [err, res] = await apiInterceptors(getKeys());
|
||||||
|
|
||||||
|
if (err) return;
|
||||||
|
|
||||||
|
const keyOptions = res?.map(({ key, label, scope }: IGetKeysResponseData) => ({
|
||||||
|
value: key,
|
||||||
|
label,
|
||||||
|
scope,
|
||||||
|
isLeaf: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setRefVariableOptions(keyOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: get keys
|
|
||||||
// useEffect(() => {
|
|
||||||
// getNodes();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// async function getNodes() {
|
|
||||||
// const [_, data] = await apiInterceptors(getFlowNodes());
|
|
||||||
// if (data && data.length > 0) {
|
|
||||||
// localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
|
|
||||||
// const operatorNodes = data.filter(node => node.flow_type === 'operator');
|
|
||||||
// const resourceNodes = data.filter(node => node.flow_type === 'resource');
|
|
||||||
// setOperators(operatorNodes);
|
|
||||||
// setResources(resourceNodes);
|
|
||||||
// setOperatorsGroup(groupNodes(operatorNodes));
|
|
||||||
// setResourcesGroup(groupNodes(resourceNodes));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function groupNodes(data: IFlowNode[]) {
|
|
||||||
// const groups: GroupType[] = [];
|
|
||||||
// const categoryMap: Record<string, { category: string; categoryLabel: string; nodes: IFlowNode[] }> = {};
|
|
||||||
// data.forEach(item => {
|
|
||||||
// const { category, category_label } = item;
|
|
||||||
// if (!categoryMap[category]) {
|
|
||||||
// categoryMap[category] = { category, categoryLabel: category_label, nodes: [] };
|
|
||||||
// groups.push(categoryMap[category]);
|
|
||||||
// }
|
|
||||||
// categoryMap[category].nodes.push(item);
|
|
||||||
// });
|
|
||||||
// return groups;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onFinish = (values: any) => {
|
const onFinish = (values: any) => {
|
||||||
console.log('Received values of form:', values);
|
const newFlowInfo = { ...flowInfo, variables: values?.parameters || [] } as IFlowUpdateParam;
|
||||||
|
setFlowInfo(newFlowInfo);
|
||||||
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
function onNameChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
|
const onNameChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
|
||||||
const name = e.target.value;
|
const name = e.target.value;
|
||||||
|
|
||||||
const result = name
|
const newValue = name
|
||||||
?.split('_')
|
?.split('_')
|
||||||
?.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
?.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
?.join(' ');
|
?.join(' ');
|
||||||
@ -72,43 +60,105 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
form.setFields([
|
form.setFields([
|
||||||
{
|
{
|
||||||
name: ['parameters', index, 'label'],
|
name: ['parameters', index, 'label'],
|
||||||
value: result,
|
value: newValue,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// change value to ref
|
const onValueTypeChange = (type: ValueType, index: number) => {
|
||||||
const type = form.getFieldValue(['parameters', index, 'value_type']);
|
const newControlTypes = [...controlTypes];
|
||||||
|
newControlTypes[index] = type;
|
||||||
|
setControlTypes(newControlTypes);
|
||||||
|
};
|
||||||
|
|
||||||
if (type === 'ref') {
|
const loadData = (selectedOptions: DefaultOptionType[]) => {
|
||||||
const parameters = form.getFieldValue('parameters');
|
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||||
const param = parameters?.[index];
|
const { value, scope } = targetOption as DefaultOptionType & { scope: string };
|
||||||
|
|
||||||
if (param) {
|
setTimeout(async () => {
|
||||||
const { name = '' } = param;
|
const [err, res] = await apiInterceptors(getVariablesByKey({ key: value as string, scope }));
|
||||||
param.value = `${DAG_PARAM_KEY}:${name}@scope:${DAG_PARAM_SCOPE}`;
|
|
||||||
|
|
||||||
form.setFieldsValue({
|
if (err) return;
|
||||||
parameters: [...parameters],
|
if (res?.total_count === 0) {
|
||||||
});
|
targetOption.isLeaf = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uniqueItems = uniqBy(res?.items, 'name');
|
||||||
|
targetOption.children = uniqueItems?.map(item => ({
|
||||||
|
value: item?.name,
|
||||||
|
label: item.label,
|
||||||
|
item: item,
|
||||||
|
}));
|
||||||
|
setRefVariableOptions([...refVariableOptions]);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRefTypeValueChange = (
|
||||||
|
value: (string | number | null)[],
|
||||||
|
selectedOptions: DefaultOptionType[],
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
// when select ref variable, must be select two options(key and variable)
|
||||||
|
if (value?.length !== 2) return;
|
||||||
|
|
||||||
|
const [selectRefKey, selectedRefVariable] = selectedOptions as DefaultOptionType[];
|
||||||
|
const selectedVariable = selectRefKey?.children?.find(
|
||||||
|
({ value }) => value === selectedRefVariable?.value,
|
||||||
|
) as DefaultOptionType & { item: IVariableItem };
|
||||||
|
|
||||||
|
// build variable string by rule
|
||||||
|
const variableStr = buildVariableString(selectedVariable?.item);
|
||||||
|
const parameters = form.getFieldValue('parameters');
|
||||||
|
const param = parameters?.[index];
|
||||||
|
if (param) {
|
||||||
|
param.value = variableStr;
|
||||||
|
param.category = selectedVariable?.item?.category;
|
||||||
|
param.value_type = selectedVariable?.item?.value_type;
|
||||||
|
|
||||||
|
form.setFieldsValue({
|
||||||
|
parameters: [...parameters],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function onValueTypeChange(type: ValueType, index: number) {
|
// Helper function to render the appropriate control component
|
||||||
if (type === 'ref') {
|
const renderVariableValue = (type: string, index: number) => {
|
||||||
const parameters = form.getFieldValue('parameters');
|
switch (type) {
|
||||||
const param = parameters?.[index];
|
case 'ref':
|
||||||
|
return (
|
||||||
if (param) {
|
<Cascader
|
||||||
const { name = '' } = param;
|
placeholder='Select Value'
|
||||||
param.value = `${DAG_PARAM_KEY}:${name}@scope:${DAG_PARAM_SCOPE}`;
|
options={refVariableOptions}
|
||||||
|
loadData={loadData}
|
||||||
form.setFieldsValue({
|
onChange={(value, selectedOptions) => onRefTypeValueChange(value, selectedOptions, index)}
|
||||||
parameters: [...parameters],
|
changeOnSelect
|
||||||
});
|
/>
|
||||||
}
|
);
|
||||||
|
case 'str':
|
||||||
|
return <Input placeholder='Parameter Value' />;
|
||||||
|
case 'int':
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
step={1}
|
||||||
|
placeholder='Parameter Value'
|
||||||
|
parser={value => value?.replace(/[^\-?\d]/g, '') || 0}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'float':
|
||||||
|
return <InputNumber placeholder='Parameter Value' style={{ width: '100%' }} />;
|
||||||
|
case 'bool':
|
||||||
|
return (
|
||||||
|
<Select placeholder='Select Value'>
|
||||||
|
<Option value={true}>True</Option>
|
||||||
|
<Option value={false}>False</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Input placeholder='Parameter Value' />;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -117,24 +167,32 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
className='flex items-center justify-center rounded-full left-4 top-4'
|
className='flex items-center justify-center rounded-full left-4 top-4'
|
||||||
style={{ zIndex: 1050 }}
|
style={{ zIndex: 1050 }}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={showModal}
|
onClick={() => setIsModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={t('Add_Global_Variable_of_Flow')}
|
title={t('Add_Global_Variable_of_Flow')}
|
||||||
open={isModalOpen}
|
|
||||||
footer={null}
|
|
||||||
width={1000}
|
width={1000}
|
||||||
|
open={isModalOpen}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
maxHeight: '70vh',
|
minHeight: '40vh',
|
||||||
|
maxHeight: '65vh',
|
||||||
overflow: 'scroll',
|
overflow: 'scroll',
|
||||||
backgroundColor: 'rgba(0,0,0,0.02)',
|
backgroundColor: 'rgba(0,0,0,0.02)',
|
||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key='cancel' onClick={() => setIsModalOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
name='dynamic_form_nest_item'
|
name='dynamic_form_nest_item'
|
||||||
@ -143,13 +201,13 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
layout='vertical'
|
layout='vertical'
|
||||||
className='mt-8'
|
className='mt-8'
|
||||||
initialValues={{ parameters: [{}] }}
|
initialValues={{ parameters: flowInfo?.variables || [{}] }}
|
||||||
>
|
>
|
||||||
<Form.List name='parameters'>
|
<Form.List name='parameters'>
|
||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
{fields.map(({ key, name, ...restField }, index) => (
|
{fields.map(({ key, name, ...restField }, index) => (
|
||||||
<Space key={key}>
|
<Space key={key} className='hover:bg-gray-100 pt-2 pl-2'>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'name']}
|
name={[name, 'name']}
|
||||||
@ -184,7 +242,7 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
rules={[{ required: true, message: 'Missing parameter type' }]}
|
rules={[{ required: true, message: 'Missing parameter type' }]}
|
||||||
>
|
>
|
||||||
<Select placeholder='Select' onChange={value => onValueTypeChange(value, index)}>
|
<Select placeholder='Select' onChange={value => onValueTypeChange(value, index)}>
|
||||||
{['str', 'int', 'float', 'bool', 'ref'].map(type => (
|
{VALUE_TYPES.map(type => (
|
||||||
<Option key={type} value={type}>
|
<Option key={type} value={type}>
|
||||||
{type}
|
{type}
|
||||||
</Option>
|
</Option>
|
||||||
@ -199,7 +257,7 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
style={{ width: 320 }}
|
style={{ width: 320 }}
|
||||||
rules={[{ required: true, message: 'Missing parameter value' }]}
|
rules={[{ required: true, message: 'Missing parameter value' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder='Parameter Value' />
|
{renderVariableValue(controlTypes[index], index)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item {...restField} name={[name, 'description']} label='描述' style={{ width: 170 }}>
|
<Form.Item {...restField} name={[name, 'description']} label='描述' style={{ width: 170 }}>
|
||||||
@ -207,6 +265,10 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||||
|
|
||||||
|
<Form.Item name={[name, 'key']} hidden initialValue='dbgpt.core.flow.params' />
|
||||||
|
<Form.Item name={[name, 'scope']} hidden initialValue='flow_priv' />
|
||||||
|
<Form.Item name={[name, 'category']} hidden initialValue='common' />
|
||||||
</Space>
|
</Space>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@ -218,15 +280,6 @@ export const AddFlowVariableModal: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|
||||||
<Form.Item wrapperCol={{ offset: 20, span: 4 }}>
|
|
||||||
<Space>
|
|
||||||
<Button onClick={() => setIsModalOpen(false)}>{t('cancel')}</Button>
|
|
||||||
<Button type='primary' htmlType='submit'>
|
|
||||||
{t('verify')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { Button, Form, Input, Modal, Radio, Space, message } from 'antd';
|
import { Button, Form, Input, Modal, Radio, message } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactFlowInstance } from 'reactflow';
|
import { ReactFlowInstance } from 'reactflow';
|
||||||
|
|
||||||
@ -46,8 +46,14 @@ export const ExportFlowModal: React.FC<Props> = ({
|
|||||||
title={t('Export_Flow')}
|
title={t('Export_Flow')}
|
||||||
open={isExportFlowModalOpen}
|
open={isExportFlowModalOpen}
|
||||||
onCancel={() => setIsExportFlowModalOpen(false)}
|
onCancel={() => setIsExportFlowModalOpen(false)}
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
footer={[
|
||||||
okButtonProps={{ className: 'hidden' }}
|
<Button key='cancel' onClick={() => setIsExportFlowModalOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -78,17 +84,6 @@ export const ExportFlowModal: React.FC<Props> = ({
|
|||||||
<Form.Item hidden name='uid'>
|
<Form.Item hidden name='uid'>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
|
||||||
<Space>
|
|
||||||
<Button htmlType='button' onClick={() => setIsExportFlowModalOpen(false)}>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='primary' htmlType='submit'>
|
|
||||||
{t('verify')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { apiInterceptors, importFlow } from '@/client/api';
|
import { apiInterceptors, importFlow } from '@/client/api';
|
||||||
|
import CanvasWrapper from '@/pages/construct/flow/canvas/index';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import { Button, Form, GetProp, Modal, Radio, Space, Upload, UploadFile, UploadProps, message } from 'antd';
|
import { Button, Form, GetProp, Modal, Radio, Upload, UploadFile, UploadProps, message } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Edge, Node } from 'reactflow';
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
import CanvasWrapper from '@/pages/construct/flow/canvas/index';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isImportModalOpen: boolean;
|
isImportModalOpen: boolean;
|
||||||
setNodes: React.Dispatch<React.SetStateAction<Node<any, string | undefined>[]>>;
|
setNodes: React.Dispatch<React.SetStateAction<Node<any, string | undefined>[]>>;
|
||||||
@ -40,10 +40,9 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
messageApi.success(t('Import_Flow_Success'));
|
messageApi.success(t('Import_Flow_Success'));
|
||||||
localStorage.setItem('importFlowData', JSON.stringify(res?.data));
|
localStorage.setItem('importFlowData', JSON.stringify(res?.data));
|
||||||
CanvasWrapper()
|
CanvasWrapper();
|
||||||
} else if (res?.err_msg) {
|
} else if (res?.err_msg) {
|
||||||
messageApi.error(res?.err_msg);
|
messageApi.error(res?.err_msg);
|
||||||
|
|
||||||
}
|
}
|
||||||
setIsImportFlowModalOpen(false);
|
setIsImportFlowModalOpen(false);
|
||||||
};
|
};
|
||||||
@ -68,8 +67,14 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
title={t('Import_Flow')}
|
title={t('Import_Flow')}
|
||||||
open={isImportModalOpen}
|
open={isImportModalOpen}
|
||||||
onCancel={() => setIsImportFlowModalOpen(false)}
|
onCancel={() => setIsImportFlowModalOpen(false)}
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
footer={[
|
||||||
okButtonProps={{ className: 'hidden' }}
|
<Button key='cancel' onClick={() => setIsImportFlowModalOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -93,21 +98,12 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name='save_flow' label={t('Save_After_Import')}>
|
<Form.Item name='save_flow' label={t('Save_After_Import')} hidden>
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
<Radio value={true}>{t('Yes')}</Radio>
|
<Radio value={true}>{t('Yes')}</Radio>
|
||||||
<Radio value={false}>{t('No')}</Radio>
|
<Radio value={false}>{t('No')}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
|
||||||
<Space>
|
|
||||||
<Button onClick={() => setIsImportFlowModalOpen(false)}>{t('cancel')}</Button>
|
|
||||||
<Button type='primary' htmlType='submit'>
|
|
||||||
{t('verify')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { addFlow, apiInterceptors, updateFlowById } from '@/client/api';
|
import { addFlow, apiInterceptors, updateFlowById } from '@/client/api';
|
||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { mapHumpToUnderline } from '@/utils/flow';
|
import { mapHumpToUnderline } from '@/utils/flow';
|
||||||
import { Button, Checkbox, Form, Input, Modal, Space, message } from 'antd';
|
import { Button, Checkbox, Form, Input, Modal, message } from 'antd';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -58,6 +58,7 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
uid: id.toString(),
|
uid: id.toString(),
|
||||||
flow_data: reactFlowObject,
|
flow_data: reactFlowObject,
|
||||||
state,
|
state,
|
||||||
|
variables: flowInfo?.variables,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
editable,
|
editable,
|
||||||
flow_data: reactFlowObject,
|
flow_data: reactFlowObject,
|
||||||
state,
|
state,
|
||||||
|
variables: flowInfo?.variables,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,11 +93,15 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
title={t('flow_modal_title')}
|
title={t('flow_modal_title')}
|
||||||
open={isSaveFlowModalOpen}
|
open={isSaveFlowModalOpen}
|
||||||
onCancel={() => {
|
onCancel={() => setIsSaveFlowModalOpen(false)}
|
||||||
setIsSaveFlowModalOpen(false);
|
footer={[
|
||||||
}}
|
<Button key='cancel' onClick={() => setIsSaveFlowModalOpen(false)}>
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
{t('cancel')}
|
||||||
okButtonProps={{ className: 'hidden' }}
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
name='flow_form'
|
name='flow_form'
|
||||||
@ -160,22 +166,6 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
htmlType='button'
|
|
||||||
onClick={() => {
|
|
||||||
setIsSaveFlowModalOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button type='primary' htmlType='submit'>
|
|
||||||
{t('verify')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -23,6 +23,6 @@ export const FlowEn = {
|
|||||||
Please_Add_Nodes_First: 'Please add nodes first',
|
Please_Add_Nodes_First: 'Please add nodes first',
|
||||||
Add_Global_Variable_of_Flow: 'Add global variable of flow',
|
Add_Global_Variable_of_Flow: 'Add global variable of flow',
|
||||||
Add_Parameter: 'Add Parameter',
|
Add_Parameter: 'Add Parameter',
|
||||||
Higher_Order_Nodes: 'Higher Order Nodes',
|
Higher_Order_Nodes: 'Higher Order',
|
||||||
All_Nodes: 'All Nodes',
|
All_Nodes: 'All',
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,6 @@ export const FlowZn = {
|
|||||||
Please_Add_Nodes_First: '请先添加节点',
|
Please_Add_Nodes_First: '请先添加节点',
|
||||||
Add_Global_Variable_of_Flow: '添加 Flow 全局变量',
|
Add_Global_Variable_of_Flow: '添加 Flow 全局变量',
|
||||||
Add_Parameter: '添加参数',
|
Add_Parameter: '添加参数',
|
||||||
Higher_Order_Nodes: '高阶节点',
|
Higher_Order_Nodes: '高阶',
|
||||||
All_Nodes: '所有节点',
|
All_Nodes: '所有',
|
||||||
};
|
};
|
||||||
|
@ -308,15 +308,14 @@ export default function AppContent() {
|
|||||||
className='w-[230px] h-[40px] border-1 border-white backdrop-filter backdrop-blur-lg bg-white bg-opacity-30 dark:border-[#6f7f95] dark:bg-[#6f7f95] dark:bg-opacity-60'
|
className='w-[230px] h-[40px] border-1 border-white backdrop-filter backdrop-blur-lg bg-white bg-opacity-30 dark:border-[#6f7f95] dark:bg-[#6f7f95] dark:bg-opacity-60'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-4 h-10'>
|
|
||||||
<Button
|
<Button
|
||||||
className='border-none text-white bg-button-gradient h-full flex items-center'
|
className='border-none text-white bg-button-gradient flex items-center'
|
||||||
icon={<PlusOutlined className='text-base' />}
|
icon={<PlusOutlined className='text-base' />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
>
|
>
|
||||||
{t('create_app')}
|
{t('create_app')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className=' w-full flex flex-wrap pb-12 mx-[-8px]'>
|
<div className=' w-full flex flex-wrap pb-12 mx-[-8px]'>
|
||||||
{apps.map(item => {
|
{apps.map(item => {
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
import CanvasNode from '@/components/flow/canvas-node';
|
import CanvasNode from '@/components/flow/canvas-node';
|
||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
|
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
|
||||||
import { ExportOutlined, FrownOutlined, ImportOutlined, FileAddOutlined,SaveOutlined } from '@ant-design/icons';
|
import { ExportOutlined, FileAddOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { Divider, Space, Tooltip, message, notification } from 'antd';
|
import { Divider, Space, Tooltip, message, notification } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@ -32,27 +32,27 @@ import 'reactflow/dist/style.css';
|
|||||||
|
|
||||||
const nodeTypes = { customNode: CanvasNode };
|
const nodeTypes = { customNode: CanvasNode };
|
||||||
const edgeTypes = { buttonedge: ButtonEdge };
|
const edgeTypes = { buttonedge: ButtonEdge };
|
||||||
|
|
||||||
const Canvas: React.FC = () => {
|
const Canvas: React.FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const id = searchParams?.get('id') || '';
|
const id = searchParams?.get('id') || '';
|
||||||
const reactFlow = useReactFlow();
|
const reactFlow = useReactFlow();
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
||||||
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
||||||
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
||||||
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
|
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
|
||||||
const [isTemplateFlowModalOpen, setIsTemplateFlowModalOpen] = useState(false);
|
const [isTemplateFlowModalOpen, setIsTemplateFlowModalOpen] = useState(false);
|
||||||
|
|
||||||
if (localStorage.getItem('importFlowData')) {
|
if (localStorage.getItem('importFlowData')) {
|
||||||
const importFlowData = JSON.parse(localStorage.getItem('importFlowData'));
|
const importFlowData = JSON.parse(localStorage.getItem('importFlowData') || '');
|
||||||
localStorage.removeItem('importFlowData');
|
localStorage.removeItem('importFlowData');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const flowData = mapUnderlineToHump(importFlowData.flow_data);
|
const flowData = mapUnderlineToHump(importFlowData.flow_data);
|
||||||
@ -61,6 +61,7 @@ const Canvas: React.FC = () => {
|
|||||||
setEdges(flowData.edges);
|
setEdges(flowData.edges);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFlowData() {
|
async function getFlowData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [_, data] = await apiInterceptors(getFlowById(id));
|
const [_, data] = await apiInterceptors(getFlowById(id));
|
||||||
@ -275,7 +276,7 @@ const Canvas: React.FC = () => {
|
|||||||
|
|
||||||
<Background color='#aaa' gap={16} />
|
<Background color='#aaa' gap={16} />
|
||||||
|
|
||||||
<AddFlowVariableModal />
|
<AddFlowVariableModal flowInfo={flowInfo} setFlowInfo={setFlowInfo} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ import { ClearOutlined, LoadingOutlined, PauseCircleOutlined, RedoOutlined, Send
|
|||||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
||||||
import { useRequest } from 'ahooks';
|
import { useRequest } from 'ahooks';
|
||||||
import { Button, Input, Popover, Spin, Tag } from 'antd';
|
import { Button, Input, Popover, Spin, Tag } from 'antd';
|
||||||
import cls from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { MobileChatContext } from '../';
|
import { MobileChatContext } from '../';
|
||||||
@ -245,7 +245,7 @@ const InputContainer: React.FC = () => {
|
|||||||
<div className='flex items-center justify-between text-lg font-bold'>
|
<div className='flex items-center justify-between text-lg font-bold'>
|
||||||
<Popover content='暂停回复' trigger={['hover']}>
|
<Popover content='暂停回复' trigger={['hover']}>
|
||||||
<PauseCircleOutlined
|
<PauseCircleOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-[#0c75fc]': canAbort,
|
'text-[#0c75fc]': canAbort,
|
||||||
'text-gray-400': !canAbort,
|
'text-gray-400': !canAbort,
|
||||||
})}
|
})}
|
||||||
@ -254,7 +254,7 @@ const InputContainer: React.FC = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Popover content='再来一次' trigger={['hover']}>
|
<Popover content='再来一次' trigger={['hover']}>
|
||||||
<RedoOutlined
|
<RedoOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-gray-400': !history.length || !canNewChat,
|
'text-gray-400': !history.length || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={redo}
|
onClick={redo}
|
||||||
@ -265,7 +265,7 @@ const InputContainer: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Popover content='清除历史' trigger={['hover']}>
|
<Popover content='清除历史' trigger={['hover']}>
|
||||||
<ClearOutlined
|
<ClearOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-gray-400': !history.length || !canNewChat,
|
'text-gray-400': !history.length || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={clearHistory}
|
onClick={clearHistory}
|
||||||
@ -276,7 +276,7 @@ const InputContainer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
<div
|
<div
|
||||||
className={cls(
|
className={classnames(
|
||||||
'flex py-2 px-3 items-center justify-between bg-white dark:bg-[#242733] dark:border-[#6f7f95] rounded-xl border',
|
'flex py-2 px-3 items-center justify-between bg-white dark:bg-[#242733] dark:border-[#6f7f95] rounded-xl border',
|
||||||
{
|
{
|
||||||
'border-[#0c75fc] dark:border-[rgba(12,117,252,0.8)]': isFocus,
|
'border-[#0c75fc] dark:border-[rgba(12,117,252,0.8)]': isFocus,
|
||||||
@ -323,7 +323,7 @@ const InputContainer: React.FC = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
className={cls('flex items-center justify-center rounded-lg bg-button-gradient border-0 ml-2', {
|
className={classnames('flex items-center justify-center rounded-lg bg-button-gradient border-0 ml-2', {
|
||||||
'opacity-40 cursor-not-allowed': !userInput.trim() || !canNewChat,
|
'opacity-40 cursor-not-allowed': !userInput.trim() || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
@ -12,6 +12,7 @@ export type IFlowUpdateParam = {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
flow_data?: IFlowData;
|
flow_data?: IFlowData;
|
||||||
state?: FlowState;
|
state?: FlowState;
|
||||||
|
variables?: IVariableItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IFlowRefreshParams = {
|
export type IFlowRefreshParams = {
|
||||||
@ -169,8 +170,9 @@ export type IFlowDataViewport = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IFlowData = {
|
export type IFlowData = {
|
||||||
nodes: Array<IFlowDataNode>;
|
nodes: IFlowDataNode[];
|
||||||
edges: Array<IFlowDataEdge>;
|
edges: IFlowDataEdge[];
|
||||||
|
variables?: IVariableItem[];
|
||||||
viewport: IFlowDataViewport;
|
viewport: IFlowDataViewport;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -200,3 +202,54 @@ export type IUploadFileResponse = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IGetKeysRequestParams = {
|
||||||
|
user_name?: string;
|
||||||
|
sys_code?: string;
|
||||||
|
category?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IGetKeysResponseData = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
value_type: string;
|
||||||
|
category: string;
|
||||||
|
scope: string;
|
||||||
|
scope_key: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IGetVariablesByKeyRequestParams = {
|
||||||
|
key: string;
|
||||||
|
scope: string;
|
||||||
|
scope_key?: string;
|
||||||
|
user_name?: string;
|
||||||
|
sys_code?: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IGetVariablesByKeyResponseData = {
|
||||||
|
items: IVariableItem[];
|
||||||
|
total_count: number;
|
||||||
|
total_pages: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IVariableItem = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description: string | null;
|
||||||
|
value_type: string;
|
||||||
|
category: string;
|
||||||
|
scope: string;
|
||||||
|
scope_key: string | null;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
enabled: boolean;
|
||||||
|
user_name: string | null;
|
||||||
|
sys_code: string | null;
|
||||||
|
id: number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IFlowData, IFlowDataNode, IFlowNode } from '@/types/flow';
|
import { IFlowData, IFlowDataNode, IFlowNode, IVariableItem } from '@/types/flow';
|
||||||
import { Node } from 'reactflow';
|
import { Node } from 'reactflow';
|
||||||
|
|
||||||
export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
|
export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
|
||||||
@ -140,3 +140,57 @@ export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string,
|
|||||||
|
|
||||||
return convert(obj);
|
return convert(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function escapeVariable(value: string, enableEscape: boolean): string {
|
||||||
|
if (!enableEscape) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.replace(/@/g, '\\@').replace(/#/g, '\\#').replace(/%/g, '\\%').replace(/:/g, '\\:');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVariableString(variableDict: IVariableItem): string {
|
||||||
|
const scopeSig = '@';
|
||||||
|
const sysCodeSig = '#';
|
||||||
|
const userSig = '%';
|
||||||
|
const kvSig = ':';
|
||||||
|
const enableEscape = true;
|
||||||
|
|
||||||
|
const specialChars = new Set([scopeSig, sysCodeSig, userSig, kvSig]);
|
||||||
|
|
||||||
|
const newVariableDict: Partial<IVariableItem> = {
|
||||||
|
key: variableDict.key || '',
|
||||||
|
name: variableDict.name || '',
|
||||||
|
scope: variableDict.scope || '',
|
||||||
|
scope_key: variableDict.scope_key || '',
|
||||||
|
sys_code: variableDict.sys_code || '',
|
||||||
|
user_name: variableDict.user_name || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for special characters in values
|
||||||
|
for (const [key, value] of Object.entries(newVariableDict)) {
|
||||||
|
if (value && [...specialChars].some(char => (value as string).includes(char))) {
|
||||||
|
if (enableEscape) {
|
||||||
|
newVariableDict[key] = escapeVariable(value as string, enableEscape);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`${key} contains special characters, error value: ${value}, special characters: ${[...specialChars].join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key, name, scope, scope_key, sys_code, user_name } = newVariableDict;
|
||||||
|
|
||||||
|
let variableStr = `${key}`;
|
||||||
|
|
||||||
|
if (name) variableStr += `${kvSig}${name}`;
|
||||||
|
if (scope || scope_key) {
|
||||||
|
variableStr += `${scopeSig}${scope}`;
|
||||||
|
if (scope_key) {
|
||||||
|
variableStr += `${kvSig}${scope_key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sys_code) variableStr += `${sysCodeSig}${sys_code}`;
|
||||||
|
if (user_name) variableStr += `${userSig}${user_name}`;
|
||||||
|
return `\${${variableStr}}`;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user