Merge branch 'main' into feat/description-field-for-env-var

# Conflicts:
#	web/i18n/ko-KR/workflow.ts
pull/21556/head
Mminamiyama 11 months ago
commit ea135c4e6b

@ -28,7 +28,7 @@ class TemplateTransformer(ABC):
def extract_result_str_from_response(cls, response: str): def extract_result_str_from_response(cls, response: str):
result = re.search(rf"{cls._result_tag}(.*){cls._result_tag}", response, re.DOTALL) result = re.search(rf"{cls._result_tag}(.*){cls._result_tag}", response, re.DOTALL)
if not result: if not result:
raise ValueError("Failed to parse result") raise ValueError(f"Failed to parse result: no result tag found in response. Response: {response[:200]}...")
return result.group(1) return result.group(1)
@classmethod @classmethod
@ -38,16 +38,53 @@ class TemplateTransformer(ABC):
:param response: response :param response: response
:return: :return:
""" """
try: try:
result = json.loads(cls.extract_result_str_from_response(response)) result_str = cls.extract_result_str_from_response(response)
except json.JSONDecodeError: result = json.loads(result_str)
raise ValueError("failed to parse response") except json.JSONDecodeError as e:
raise ValueError(f"Failed to parse JSON response: {str(e)}. Response content: {result_str[:200]}...")
except ValueError as e:
# Re-raise ValueError from extract_result_str_from_response
raise e
except Exception as e:
raise ValueError(f"Unexpected error during response transformation: {str(e)}")
# Check if the result contains an error
if isinstance(result, dict) and "error" in result:
raise ValueError(f"JavaScript execution error: {result['error']}")
if not isinstance(result, dict): if not isinstance(result, dict):
raise ValueError("result must be a dict") raise ValueError(f"Result must be a dict, got {type(result).__name__}")
if not all(isinstance(k, str) for k in result): if not all(isinstance(k, str) for k in result):
raise ValueError("result keys must be strings") raise ValueError("Result keys must be strings")
# Post-process the result to convert scientific notation strings back to numbers
result = cls._post_process_result(result)
return result return result
@classmethod
def _post_process_result(cls, result: dict[Any, Any]) -> dict[Any, Any]:
"""
Post-process the result to convert scientific notation strings back to numbers
"""
def convert_scientific_notation(value):
if isinstance(value, str):
# Check if the string looks like scientific notation
if re.match(r"^-?\d+\.?\d*e[+-]\d+$", value, re.IGNORECASE):
try:
return float(value)
except ValueError:
pass
elif isinstance(value, dict):
return {k: convert_scientific_notation(v) for k, v in value.items()}
elif isinstance(value, list):
return [convert_scientific_notation(v) for v in value]
return value
return convert_scientific_notation(result) # type: ignore[no-any-return]
@classmethod @classmethod
@abstractmethod @abstractmethod
def get_runner_script(cls) -> str: def get_runner_script(cls) -> str:

@ -0,0 +1,486 @@
import json
import logging
from collections.abc import Sequence
from typing import Optional
from urllib.parse import urljoin
from opentelemetry.trace import Status, StatusCode
from sqlalchemy.orm import Session, sessionmaker
from core.ops.aliyun_trace.data_exporter.traceclient import (
TraceClient,
convert_datetime_to_nanoseconds,
convert_to_span_id,
convert_to_trace_id,
generate_span_id,
)
from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData
from core.ops.aliyun_trace.entities.semconv import (
GEN_AI_COMPLETION,
GEN_AI_FRAMEWORK,
GEN_AI_MODEL_NAME,
GEN_AI_PROMPT,
GEN_AI_PROMPT_TEMPLATE_TEMPLATE,
GEN_AI_PROMPT_TEMPLATE_VARIABLE,
GEN_AI_RESPONSE_FINISH_REASON,
GEN_AI_SESSION_ID,
GEN_AI_SPAN_KIND,
GEN_AI_SYSTEM,
GEN_AI_USAGE_INPUT_TOKENS,
GEN_AI_USAGE_OUTPUT_TOKENS,
GEN_AI_USAGE_TOTAL_TOKENS,
GEN_AI_USER_ID,
INPUT_VALUE,
OUTPUT_VALUE,
RETRIEVAL_DOCUMENT,
RETRIEVAL_QUERY,
TOOL_DESCRIPTION,
TOOL_NAME,
TOOL_PARAMETERS,
GenAISpanKind,
)
from core.ops.base_trace_instance import BaseTraceInstance
from core.ops.entities.config_entity import AliyunConfig
from core.ops.entities.trace_entity import (
BaseTraceInfo,
DatasetRetrievalTraceInfo,
GenerateNameTraceInfo,
MessageTraceInfo,
ModerationTraceInfo,
SuggestedQuestionTraceInfo,
ToolTraceInfo,
WorkflowTraceInfo,
)
from core.rag.models.document import Document
from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.entities.workflow_node_execution import (
WorkflowNodeExecution,
WorkflowNodeExecutionMetadataKey,
WorkflowNodeExecutionStatus,
)
from core.workflow.nodes import NodeType
from models import Account, App, EndUser, TenantAccountJoin, WorkflowNodeExecutionTriggeredFrom, db
logger = logging.getLogger(__name__)
class AliyunDataTrace(BaseTraceInstance):
def __init__(
self,
aliyun_config: AliyunConfig,
):
super().__init__(aliyun_config)
base_url = aliyun_config.endpoint.rstrip("/")
endpoint = urljoin(base_url, f"adapt_{aliyun_config.license_key}/api/otlp/traces")
self.trace_client = TraceClient(service_name=aliyun_config.app_name, endpoint=endpoint)
def trace(self, trace_info: BaseTraceInfo):
if isinstance(trace_info, WorkflowTraceInfo):
self.workflow_trace(trace_info)
if isinstance(trace_info, MessageTraceInfo):
self.message_trace(trace_info)
if isinstance(trace_info, ModerationTraceInfo):
pass
if isinstance(trace_info, SuggestedQuestionTraceInfo):
self.suggested_question_trace(trace_info)
if isinstance(trace_info, DatasetRetrievalTraceInfo):
self.dataset_retrieval_trace(trace_info)
if isinstance(trace_info, ToolTraceInfo):
self.tool_trace(trace_info)
if isinstance(trace_info, GenerateNameTraceInfo):
pass
def api_check(self):
return self.trace_client.api_check()
def get_project_url(self):
try:
return self.trace_client.get_project_url()
except Exception as e:
logger.info(f"Aliyun get run url failed: {str(e)}", exc_info=True)
raise ValueError(f"Aliyun get run url failed: {str(e)}")
def workflow_trace(self, trace_info: WorkflowTraceInfo):
trace_id = convert_to_trace_id(trace_info.workflow_run_id)
workflow_span_id = convert_to_span_id(trace_info.workflow_run_id, "workflow")
self.add_workflow_span(trace_id, workflow_span_id, trace_info)
workflow_node_executions = self.get_workflow_node_executions(trace_info)
for node_execution in workflow_node_executions:
node_span = self.build_workflow_node_span(node_execution, trace_id, trace_info, workflow_span_id)
self.trace_client.add_span(node_span)
def message_trace(self, trace_info: MessageTraceInfo):
message_data = trace_info.message_data
if message_data is None:
return
message_id = trace_info.message_id
user_id = message_data.from_account_id
if message_data.from_end_user_id:
end_user_data: Optional[EndUser] = (
db.session.query(EndUser).filter(EndUser.id == message_data.from_end_user_id).first()
)
if end_user_data is not None:
user_id = end_user_data.session_id
status: Status = Status(StatusCode.OK)
if trace_info.error:
status = Status(StatusCode.ERROR, trace_info.error)
trace_id = convert_to_trace_id(message_id)
message_span_id = convert_to_span_id(message_id, "message")
message_span = SpanData(
trace_id=trace_id,
parent_span_id=None,
span_id=message_span_id,
name="message",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""),
GEN_AI_USER_ID: str(user_id),
GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
GEN_AI_FRAMEWORK: "dify",
INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
OUTPUT_VALUE: str(trace_info.outputs),
},
status=status,
)
self.trace_client.add_span(message_span)
app_model_config = getattr(trace_info.message_data, "app_model_config", {})
pre_prompt = getattr(app_model_config, "pre_prompt", "")
inputs_data = getattr(trace_info.message_data, "inputs", {})
llm_span = SpanData(
trace_id=trace_id,
parent_span_id=message_span_id,
span_id=convert_to_span_id(message_id, "llm"),
name="llm",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""),
GEN_AI_USER_ID: str(user_id),
GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
GEN_AI_FRAMEWORK: "dify",
GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name", ""),
GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider", ""),
GEN_AI_USAGE_INPUT_TOKENS: str(trace_info.message_tokens),
GEN_AI_USAGE_OUTPUT_TOKENS: str(trace_info.answer_tokens),
GEN_AI_USAGE_TOTAL_TOKENS: str(trace_info.total_tokens),
GEN_AI_PROMPT_TEMPLATE_VARIABLE: json.dumps(inputs_data, ensure_ascii=False),
GEN_AI_PROMPT_TEMPLATE_TEMPLATE: pre_prompt,
GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False),
GEN_AI_COMPLETION: str(trace_info.outputs),
INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
OUTPUT_VALUE: str(trace_info.outputs),
},
status=status,
)
self.trace_client.add_span(llm_span)
def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo):
if trace_info.message_data is None:
return
message_id = trace_info.message_id
documents_data = extract_retrieval_documents(trace_info.documents)
dataset_retrieval_span = SpanData(
trace_id=convert_to_trace_id(message_id),
parent_span_id=convert_to_span_id(message_id, "message"),
span_id=generate_span_id(),
name="dataset_retrieval",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value,
GEN_AI_FRAMEWORK: "dify",
RETRIEVAL_QUERY: str(trace_info.inputs),
RETRIEVAL_DOCUMENT: json.dumps(documents_data, ensure_ascii=False),
INPUT_VALUE: str(trace_info.inputs),
OUTPUT_VALUE: json.dumps(documents_data, ensure_ascii=False),
},
)
self.trace_client.add_span(dataset_retrieval_span)
def tool_trace(self, trace_info: ToolTraceInfo):
if trace_info.message_data is None:
return
message_id = trace_info.message_id
status: Status = Status(StatusCode.OK)
if trace_info.error:
status = Status(StatusCode.ERROR, trace_info.error)
tool_span = SpanData(
trace_id=convert_to_trace_id(message_id),
parent_span_id=convert_to_span_id(message_id, "message"),
span_id=generate_span_id(),
name=trace_info.tool_name,
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value,
GEN_AI_FRAMEWORK: "dify",
TOOL_NAME: trace_info.tool_name,
TOOL_DESCRIPTION: json.dumps(trace_info.tool_config, ensure_ascii=False),
TOOL_PARAMETERS: json.dumps(trace_info.tool_inputs, ensure_ascii=False),
INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
OUTPUT_VALUE: str(trace_info.tool_outputs),
},
status=status,
)
self.trace_client.add_span(tool_span)
def get_workflow_node_executions(self, trace_info: WorkflowTraceInfo) -> Sequence[WorkflowNodeExecution]:
# through workflow_run_id get all_nodes_execution using repository
session_factory = sessionmaker(bind=db.engine)
# Find the app's creator account
with Session(db.engine, expire_on_commit=False) as session:
# Get the app to find its creator
app_id = trace_info.metadata.get("app_id")
if not app_id:
raise ValueError("No app_id found in trace_info metadata")
app = session.query(App).filter(App.id == app_id).first()
if not app:
raise ValueError(f"App with id {app_id} not found")
if not app.created_by:
raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
service_account = session.query(Account).filter(Account.id == app.created_by).first()
if not service_account:
raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
current_tenant = (
session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first()
)
if not current_tenant:
raise ValueError(f"Current tenant not found for account {service_account.id}")
service_account.set_tenant_id(current_tenant.tenant_id)
workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
session_factory=session_factory,
user=service_account,
app_id=trace_info.metadata.get("app_id"),
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
)
# Get all executions for this workflow run
workflow_node_executions = workflow_node_execution_repository.get_by_workflow_run(
workflow_run_id=trace_info.workflow_run_id
)
return workflow_node_executions
def build_workflow_node_span(
self, node_execution: WorkflowNodeExecution, trace_id: int, trace_info: WorkflowTraceInfo, workflow_span_id: int
):
try:
if node_execution.node_type == NodeType.LLM:
node_span = self.build_workflow_llm_span(trace_id, workflow_span_id, trace_info, node_execution)
elif node_execution.node_type == NodeType.KNOWLEDGE_RETRIEVAL:
node_span = self.build_workflow_retrieval_span(trace_id, workflow_span_id, trace_info, node_execution)
elif node_execution.node_type == NodeType.TOOL:
node_span = self.build_workflow_tool_span(trace_id, workflow_span_id, trace_info, node_execution)
else:
node_span = self.build_workflow_task_span(trace_id, workflow_span_id, trace_info, node_execution)
return node_span
except Exception:
return None
def get_workflow_node_status(self, node_execution: WorkflowNodeExecution) -> Status:
span_status: Status = Status(StatusCode.UNSET)
if node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED:
span_status = Status(StatusCode.OK)
elif node_execution.status in [WorkflowNodeExecutionStatus.FAILED, WorkflowNodeExecutionStatus.EXCEPTION]:
span_status = Status(StatusCode.ERROR, str(node_execution.error))
return span_status
def build_workflow_task_span(
self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
) -> SpanData:
return SpanData(
trace_id=trace_id,
parent_span_id=workflow_span_id,
span_id=convert_to_span_id(node_execution.id, "node"),
name=node_execution.title,
start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
attributes={
GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""),
GEN_AI_SPAN_KIND: GenAISpanKind.TASK.value,
GEN_AI_FRAMEWORK: "dify",
INPUT_VALUE: json.dumps(node_execution.inputs, ensure_ascii=False),
OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False),
},
status=self.get_workflow_node_status(node_execution),
)
def build_workflow_tool_span(
self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
) -> SpanData:
tool_des = {}
if node_execution.metadata:
tool_des = node_execution.metadata.get(WorkflowNodeExecutionMetadataKey.TOOL_INFO, {})
return SpanData(
trace_id=trace_id,
parent_span_id=workflow_span_id,
span_id=convert_to_span_id(node_execution.id, "node"),
name=node_execution.title,
start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
attributes={
GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value,
GEN_AI_FRAMEWORK: "dify",
TOOL_NAME: node_execution.title,
TOOL_DESCRIPTION: json.dumps(tool_des, ensure_ascii=False),
TOOL_PARAMETERS: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False),
INPUT_VALUE: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False),
OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False),
},
status=self.get_workflow_node_status(node_execution),
)
def build_workflow_retrieval_span(
self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
) -> SpanData:
input_value = ""
if node_execution.inputs:
input_value = str(node_execution.inputs.get("query", ""))
output_value = ""
if node_execution.outputs:
output_value = json.dumps(node_execution.outputs.get("result", []), ensure_ascii=False)
return SpanData(
trace_id=trace_id,
parent_span_id=workflow_span_id,
span_id=convert_to_span_id(node_execution.id, "node"),
name=node_execution.title,
start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
attributes={
GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value,
GEN_AI_FRAMEWORK: "dify",
RETRIEVAL_QUERY: input_value,
RETRIEVAL_DOCUMENT: output_value,
INPUT_VALUE: input_value,
OUTPUT_VALUE: output_value,
},
status=self.get_workflow_node_status(node_execution),
)
def build_workflow_llm_span(
self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
) -> SpanData:
process_data = node_execution.process_data or {}
outputs = node_execution.outputs or {}
return SpanData(
trace_id=trace_id,
parent_span_id=workflow_span_id,
span_id=convert_to_span_id(node_execution.id, "node"),
name=node_execution.title,
start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
attributes={
GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""),
GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
GEN_AI_FRAMEWORK: "dify",
GEN_AI_MODEL_NAME: process_data.get("model_name", ""),
GEN_AI_SYSTEM: process_data.get("model_provider", ""),
GEN_AI_USAGE_INPUT_TOKENS: str(outputs.get("usage", {}).get("prompt_tokens", 0)),
GEN_AI_USAGE_OUTPUT_TOKENS: str(outputs.get("usage", {}).get("completion_tokens", 0)),
GEN_AI_USAGE_TOTAL_TOKENS: str(outputs.get("usage", {}).get("total_tokens", 0)),
GEN_AI_PROMPT: json.dumps(process_data.get("prompts", []), ensure_ascii=False),
GEN_AI_COMPLETION: str(outputs.get("text", "")),
GEN_AI_RESPONSE_FINISH_REASON: outputs.get("finish_reason", ""),
INPUT_VALUE: json.dumps(process_data.get("prompts", []), ensure_ascii=False),
OUTPUT_VALUE: str(outputs.get("text", "")),
},
status=self.get_workflow_node_status(node_execution),
)
def add_workflow_span(self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo):
message_span_id = None
if trace_info.message_id:
message_span_id = convert_to_span_id(trace_info.message_id, "message")
user_id = trace_info.metadata.get("user_id")
status: Status = Status(StatusCode.OK)
if trace_info.error:
status = Status(StatusCode.ERROR, trace_info.error)
if message_span_id: # chatflow
message_span = SpanData(
trace_id=trace_id,
parent_span_id=None,
span_id=message_span_id,
name="message",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""),
GEN_AI_USER_ID: str(user_id),
GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
GEN_AI_FRAMEWORK: "dify",
INPUT_VALUE: trace_info.workflow_run_inputs.get("sys.query", ""),
OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
},
status=status,
)
self.trace_client.add_span(message_span)
workflow_span = SpanData(
trace_id=trace_id,
parent_span_id=message_span_id,
span_id=workflow_span_id,
name="workflow",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_USER_ID: str(user_id),
GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
GEN_AI_FRAMEWORK: "dify",
INPUT_VALUE: json.dumps(trace_info.workflow_run_inputs, ensure_ascii=False),
OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
},
status=status,
)
self.trace_client.add_span(workflow_span)
def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo):
message_id = trace_info.message_id
status: Status = Status(StatusCode.OK)
if trace_info.error:
status = Status(StatusCode.ERROR, trace_info.error)
suggested_question_span = SpanData(
trace_id=convert_to_trace_id(message_id),
parent_span_id=convert_to_span_id(message_id, "message"),
span_id=convert_to_span_id(message_id, "suggested_question"),
name="suggested_question",
start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
attributes={
GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
GEN_AI_FRAMEWORK: "dify",
GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name", ""),
GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider", ""),
GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False),
GEN_AI_COMPLETION: json.dumps(trace_info.suggested_question, ensure_ascii=False),
INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
OUTPUT_VALUE: json.dumps(trace_info.suggested_question, ensure_ascii=False),
},
status=status,
)
self.trace_client.add_span(suggested_question_span)
def extract_retrieval_documents(documents: list[Document]):
documents_data = []
for document in documents:
document_data = {
"content": document.page_content,
"metadata": {
"dataset_id": document.metadata.get("dataset_id"),
"doc_id": document.metadata.get("doc_id"),
"document_id": document.metadata.get("document_id"),
},
"score": document.metadata.get("score"),
}
documents_data.append(document_data)
return documents_data

@ -0,0 +1,200 @@
import hashlib
import logging
import random
import socket
import threading
import uuid
from collections import deque
from collections.abc import Sequence
from datetime import datetime
from typing import Optional
import requests
from opentelemetry import trace as trace_api
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.semconv.resource import ResourceAttributes
from configs import dify_config
from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData
INVALID_SPAN_ID = 0x0000000000000000
INVALID_TRACE_ID = 0x00000000000000000000000000000000
logger = logging.getLogger(__name__)
class TraceClient:
def __init__(
self,
service_name: str,
endpoint: str,
max_queue_size: int = 1000,
schedule_delay_sec: int = 5,
max_export_batch_size: int = 50,
):
self.endpoint = endpoint
self.resource = Resource(
attributes={
ResourceAttributes.SERVICE_NAME: service_name,
ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}",
ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
ResourceAttributes.HOST_NAME: socket.gethostname(),
}
)
self.span_builder = SpanBuilder(self.resource)
self.exporter = OTLPSpanExporter(endpoint=endpoint)
self.max_queue_size = max_queue_size
self.schedule_delay_sec = schedule_delay_sec
self.max_export_batch_size = max_export_batch_size
self.queue: deque = deque(maxlen=max_queue_size)
self.condition = threading.Condition(threading.Lock())
self.done = False
self.worker_thread = threading.Thread(target=self._worker, daemon=True)
self.worker_thread.start()
self._spans_dropped = False
def export(self, spans: Sequence[ReadableSpan]):
self.exporter.export(spans)
def api_check(self):
try:
response = requests.head(self.endpoint, timeout=5)
if response.status_code == 405:
return True
else:
logger.debug(f"AliyunTrace API check failed: Unexpected status code: {response.status_code}")
return False
except requests.exceptions.RequestException as e:
logger.debug(f"AliyunTrace API check failed: {str(e)}")
raise ValueError(f"AliyunTrace API check failed: {str(e)}")
def get_project_url(self):
return "https://arms.console.aliyun.com/#/llm"
def add_span(self, span_data: SpanData):
if span_data is None:
return
span: ReadableSpan = self.span_builder.build_span(span_data)
with self.condition:
if len(self.queue) == self.max_queue_size:
if not self._spans_dropped:
logger.warning("Queue is full, likely spans will be dropped.")
self._spans_dropped = True
self.queue.appendleft(span)
if len(self.queue) >= self.max_export_batch_size:
self.condition.notify()
def _worker(self):
while not self.done:
with self.condition:
if len(self.queue) < self.max_export_batch_size and not self.done:
self.condition.wait(timeout=self.schedule_delay_sec)
self._export_batch()
def _export_batch(self):
spans_to_export: list[ReadableSpan] = []
with self.condition:
while len(spans_to_export) < self.max_export_batch_size and self.queue:
spans_to_export.append(self.queue.pop())
if spans_to_export:
try:
self.exporter.export(spans_to_export)
except Exception as e:
logger.debug(f"Error exporting spans: {e}")
def shutdown(self):
with self.condition:
self.done = True
self.condition.notify_all()
self.worker_thread.join()
self._export_batch()
self.exporter.shutdown()
class SpanBuilder:
def __init__(self, resource):
self.resource = resource
self.instrumentation_scope = InstrumentationScope(
__name__,
"",
None,
None,
)
def build_span(self, span_data: SpanData) -> ReadableSpan:
span_context = trace_api.SpanContext(
trace_id=span_data.trace_id,
span_id=span_data.span_id,
is_remote=False,
trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED),
trace_state=None,
)
parent_span_context = None
if span_data.parent_span_id is not None:
parent_span_context = trace_api.SpanContext(
trace_id=span_data.trace_id,
span_id=span_data.parent_span_id,
is_remote=False,
trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED),
trace_state=None,
)
span = ReadableSpan(
name=span_data.name,
context=span_context,
parent=parent_span_context,
resource=self.resource,
attributes=span_data.attributes,
events=span_data.events,
links=span_data.links,
kind=trace_api.SpanKind.INTERNAL,
status=span_data.status,
start_time=span_data.start_time,
end_time=span_data.end_time,
instrumentation_scope=self.instrumentation_scope,
)
return span
def generate_span_id() -> int:
span_id = random.getrandbits(64)
while span_id == INVALID_SPAN_ID:
span_id = random.getrandbits(64)
return span_id
def convert_to_trace_id(uuid_v4: Optional[str]) -> int:
try:
uuid_obj = uuid.UUID(uuid_v4)
return uuid_obj.int
except Exception as e:
raise ValueError(f"Invalid UUID input: {e}")
def convert_to_span_id(uuid_v4: Optional[str], span_type: str) -> int:
try:
uuid_obj = uuid.UUID(uuid_v4)
except Exception as e:
raise ValueError(f"Invalid UUID input: {e}")
combined_key = f"{uuid_obj.hex}-{span_type}"
hash_bytes = hashlib.sha256(combined_key.encode("utf-8")).digest()
span_id = int.from_bytes(hash_bytes[:8], byteorder="big", signed=False)
return span_id
def convert_datetime_to_nanoseconds(start_time_a: Optional[datetime]) -> Optional[int]:
if start_time_a is None:
return None
timestamp_in_seconds = start_time_a.timestamp()
timestamp_in_nanoseconds = int(timestamp_in_seconds * 1e9)
return timestamp_in_nanoseconds

@ -0,0 +1,21 @@
from collections.abc import Sequence
from typing import Optional
from opentelemetry import trace as trace_api
from opentelemetry.sdk.trace import Event, Status, StatusCode
from pydantic import BaseModel, Field
class SpanData(BaseModel):
model_config = {"arbitrary_types_allowed": True}
trace_id: int = Field(..., description="The unique identifier for the trace.")
parent_span_id: Optional[int] = Field(None, description="The ID of the parent span, if any.")
span_id: int = Field(..., description="The unique identifier for this span.")
name: str = Field(..., description="The name of the span.")
attributes: dict[str, str] = Field(default_factory=dict, description="Attributes associated with the span.")
events: Sequence[Event] = Field(default_factory=list, description="Events recorded in the span.")
links: Sequence[trace_api.Link] = Field(default_factory=list, description="Links to other spans.")
status: Status = Field(default=Status(StatusCode.UNSET), description="The status of the span.")
start_time: Optional[int] = Field(..., description="The start time of the span in nanoseconds.")
end_time: Optional[int] = Field(..., description="The end time of the span in nanoseconds.")

@ -0,0 +1,64 @@
from enum import Enum
# public
GEN_AI_SESSION_ID = "gen_ai.session.id"
GEN_AI_USER_ID = "gen_ai.user.id"
GEN_AI_USER_NAME = "gen_ai.user.name"
GEN_AI_SPAN_KIND = "gen_ai.span.kind"
GEN_AI_FRAMEWORK = "gen_ai.framework"
# Chain
INPUT_VALUE = "input.value"
OUTPUT_VALUE = "output.value"
# Retriever
RETRIEVAL_QUERY = "retrieval.query"
RETRIEVAL_DOCUMENT = "retrieval.document"
# LLM
GEN_AI_MODEL_NAME = "gen_ai.model_name"
GEN_AI_SYSTEM = "gen_ai.system"
GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens"
GEN_AI_PROMPT_TEMPLATE_TEMPLATE = "gen_ai.prompt_template.template"
GEN_AI_PROMPT_TEMPLATE_VARIABLE = "gen_ai.prompt_template.variable"
GEN_AI_PROMPT = "gen_ai.prompt"
GEN_AI_COMPLETION = "gem_ai.completion"
GEN_AI_RESPONSE_FINISH_REASON = "gen_ai.response.finish_reason"
# Tool
TOOL_NAME = "tool.name"
TOOL_DESCRIPTION = "tool.description"
TOOL_PARAMETERS = "tool.parameters"
class GenAISpanKind(Enum):
CHAIN = "CHAIN"
RETRIEVER = "RETRIEVER"
RERANKER = "RERANKER"
LLM = "LLM"
EMBEDDING = "EMBEDDING"
TOOL = "TOOL"
AGENT = "AGENT"
TASK = "TASK"

@ -2,6 +2,8 @@ from enum import StrEnum
from pydantic import BaseModel, ValidationInfo, field_validator from pydantic import BaseModel, ValidationInfo, field_validator
from core.ops.utils import validate_project_name, validate_url, validate_url_with_path
class TracingProviderEnum(StrEnum): class TracingProviderEnum(StrEnum):
ARIZE = "arize" ARIZE = "arize"
@ -10,14 +12,41 @@ class TracingProviderEnum(StrEnum):
LANGSMITH = "langsmith" LANGSMITH = "langsmith"
OPIK = "opik" OPIK = "opik"
WEAVE = "weave" WEAVE = "weave"
ALIYUN = "aliyun"
class BaseTracingConfig(BaseModel): class BaseTracingConfig(BaseModel):
""" """
Base model class for tracing Base model class for tracing configurations
"""
@classmethod
def validate_endpoint_url(cls, v: str, default_url: str) -> str:
"""
Common endpoint URL validation logic
Args:
v: URL value to validate
default_url: Default URL to use if input is None or empty
Returns:
Validated and normalized URL
""" """
return validate_url(v, default_url)
... @classmethod
def validate_project_field(cls, v: str, default_name: str) -> str:
"""
Common project name validation logic
Args:
v: Project name to validate
default_name: Default name to use if input is None or empty
Returns:
Validated project name
"""
return validate_project_name(v, default_name)
class ArizeConfig(BaseTracingConfig): class ArizeConfig(BaseTracingConfig):
@ -33,23 +62,12 @@ class ArizeConfig(BaseTracingConfig):
@field_validator("project") @field_validator("project")
@classmethod @classmethod
def project_validator(cls, v, info: ValidationInfo): def project_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_project_field(v, "default")
v = "default"
return v
@field_validator("endpoint") @field_validator("endpoint")
@classmethod @classmethod
def endpoint_validator(cls, v, info: ValidationInfo): def endpoint_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_endpoint_url(v, "https://otlp.arize.com")
v = "https://otlp.arize.com"
if not v.startswith(("https://", "http://")):
raise ValueError("endpoint must start with https:// or http://")
if "/" in v[8:]:
parts = v.split("/")
v = parts[0] + "//" + parts[2]
return v
class PhoenixConfig(BaseTracingConfig): class PhoenixConfig(BaseTracingConfig):
@ -64,23 +82,12 @@ class PhoenixConfig(BaseTracingConfig):
@field_validator("project") @field_validator("project")
@classmethod @classmethod
def project_validator(cls, v, info: ValidationInfo): def project_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_project_field(v, "default")
v = "default"
return v
@field_validator("endpoint") @field_validator("endpoint")
@classmethod @classmethod
def endpoint_validator(cls, v, info: ValidationInfo): def endpoint_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_endpoint_url(v, "https://app.phoenix.arize.com")
v = "https://app.phoenix.arize.com"
if not v.startswith(("https://", "http://")):
raise ValueError("endpoint must start with https:// or http://")
if "/" in v[8:]:
parts = v.split("/")
v = parts[0] + "//" + parts[2]
return v
class LangfuseConfig(BaseTracingConfig): class LangfuseConfig(BaseTracingConfig):
@ -94,13 +101,8 @@ class LangfuseConfig(BaseTracingConfig):
@field_validator("host") @field_validator("host")
@classmethod @classmethod
def set_value(cls, v, info: ValidationInfo): def host_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_endpoint_url(v, "https://api.langfuse.com")
v = "https://api.langfuse.com"
if not v.startswith("https://") and not v.startswith("http://"):
raise ValueError("host must start with https:// or http://")
return v
class LangSmithConfig(BaseTracingConfig): class LangSmithConfig(BaseTracingConfig):
@ -114,13 +116,9 @@ class LangSmithConfig(BaseTracingConfig):
@field_validator("endpoint") @field_validator("endpoint")
@classmethod @classmethod
def set_value(cls, v, info: ValidationInfo): def endpoint_validator(cls, v, info: ValidationInfo):
if v is None or v == "": # LangSmith only allows HTTPS
v = "https://api.smith.langchain.com" return validate_url(v, "https://api.smith.langchain.com", allowed_schemes=("https",))
if not v.startswith("https://"):
raise ValueError("endpoint must start with https://")
return v
class OpikConfig(BaseTracingConfig): class OpikConfig(BaseTracingConfig):
@ -136,22 +134,12 @@ class OpikConfig(BaseTracingConfig):
@field_validator("project") @field_validator("project")
@classmethod @classmethod
def project_validator(cls, v, info: ValidationInfo): def project_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return cls.validate_project_field(v, "Default Project")
v = "Default Project"
return v
@field_validator("url") @field_validator("url")
@classmethod @classmethod
def url_validator(cls, v, info: ValidationInfo): def url_validator(cls, v, info: ValidationInfo):
if v is None or v == "": return validate_url_with_path(v, "https://www.comet.com/opik/api/", required_suffix="/api/")
v = "https://www.comet.com/opik/api/"
if not v.startswith(("https://", "http://")):
raise ValueError("url must start with https:// or http://")
if not v.endswith("/api/"):
raise ValueError("url should ends with /api/")
return v
class WeaveConfig(BaseTracingConfig): class WeaveConfig(BaseTracingConfig):
@ -167,22 +155,27 @@ class WeaveConfig(BaseTracingConfig):
@field_validator("endpoint") @field_validator("endpoint")
@classmethod @classmethod
def set_value(cls, v, info: ValidationInfo): def endpoint_validator(cls, v, info: ValidationInfo):
if v is None or v == "": # Weave only allows HTTPS for endpoint
v = "https://trace.wandb.ai" return validate_url(v, "https://trace.wandb.ai", allowed_schemes=("https",))
if not v.startswith("https://"):
raise ValueError("endpoint must start with https://")
return v
@field_validator("host") @field_validator("host")
@classmethod @classmethod
def validate_host(cls, v, info: ValidationInfo): def host_validator(cls, v, info: ValidationInfo):
if v is not None and v != "": if v is not None and v.strip() != "":
if not v.startswith(("https://", "http://")): return validate_url(v, v, allowed_schemes=("https", "http"))
raise ValueError("host must start with https:// or http://")
return v return v
class AliyunConfig(BaseTracingConfig):
"""
Model class for Aliyun tracing config.
"""
app_name: str = "dify_app"
license_key: str
endpoint: str
OPS_FILE_PATH = "ops_trace/" OPS_FILE_PATH = "ops_trace/"
OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE" OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE"

@ -41,28 +41,6 @@ from tasks.ops_trace_task import process_trace_tasks
class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]): class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]):
def __getitem__(self, provider: str) -> dict[str, Any]: def __getitem__(self, provider: str) -> dict[str, Any]:
match provider: match provider:
case TracingProviderEnum.ARIZE:
from core.ops.arize_phoenix_trace.arize_phoenix_trace import ArizePhoenixDataTrace
from core.ops.entities.config_entity import ArizeConfig
return {
"config_class": ArizeConfig,
"secret_keys": ["api_key", "space_id"],
"other_keys": ["project", "endpoint"],
"trace_instance": ArizePhoenixDataTrace,
}
case TracingProviderEnum.PHOENIX:
from core.ops.arize_phoenix_trace.arize_phoenix_trace import ArizePhoenixDataTrace
from core.ops.entities.config_entity import PhoenixConfig
return {
"config_class": PhoenixConfig,
"secret_keys": ["api_key"],
"other_keys": ["project", "endpoint"],
"trace_instance": ArizePhoenixDataTrace,
}
case TracingProviderEnum.LANGFUSE: case TracingProviderEnum.LANGFUSE:
from core.ops.entities.config_entity import LangfuseConfig from core.ops.entities.config_entity import LangfuseConfig
from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace
@ -126,6 +104,17 @@ class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]):
"other_keys": ["project", "endpoint"], "other_keys": ["project", "endpoint"],
"trace_instance": ArizePhoenixDataTrace, "trace_instance": ArizePhoenixDataTrace,
} }
case TracingProviderEnum.ALIYUN:
from core.ops.aliyun_trace.aliyun_trace import AliyunDataTrace
from core.ops.entities.config_entity import AliyunConfig
return {
"config_class": AliyunConfig,
"secret_keys": ["license_key"],
"other_keys": ["endpoint", "app_name"],
"trace_instance": AliyunDataTrace,
}
case _: case _:
raise KeyError(f"Unsupported tracing provider: {provider}") raise KeyError(f"Unsupported tracing provider: {provider}")

@ -1,6 +1,7 @@
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime from datetime import datetime
from typing import Optional, Union from typing import Optional, Union
from urllib.parse import urlparse
from extensions.ext_database import db from extensions.ext_database import db
from models.model import Message from models.model import Message
@ -60,3 +61,83 @@ def generate_dotted_order(
return current_segment return current_segment
return f"{parent_dotted_order}.{current_segment}" return f"{parent_dotted_order}.{current_segment}"
def validate_url(url: str, default_url: str, allowed_schemes: tuple = ("https", "http")) -> str:
"""
Validate and normalize URL with proper error handling
Args:
url: The URL to validate
default_url: Default URL to use if input is None or empty
allowed_schemes: Tuple of allowed URL schemes (default: https, http)
Returns:
Normalized URL string
Raises:
ValueError: If URL format is invalid or scheme not allowed
"""
if not url or url.strip() == "":
return default_url
# Parse URL to validate format
parsed = urlparse(url)
# Check if scheme is allowed
if parsed.scheme not in allowed_schemes:
raise ValueError(f"URL scheme must be one of: {', '.join(allowed_schemes)}")
# Reconstruct URL with only scheme, netloc (removing path, query, fragment)
normalized_url = f"{parsed.scheme}://{parsed.netloc}"
return normalized_url
def validate_url_with_path(url: str, default_url: str, required_suffix: str | None = None) -> str:
"""
Validate URL that may include path components
Args:
url: The URL to validate
default_url: Default URL to use if input is None or empty
required_suffix: Optional suffix that URL must end with
Returns:
Validated URL string
Raises:
ValueError: If URL format is invalid or doesn't match required suffix
"""
if not url or url.strip() == "":
return default_url
# Parse URL to validate format
parsed = urlparse(url)
# Check if scheme is allowed
if parsed.scheme not in ("https", "http"):
raise ValueError("URL must start with https:// or http://")
# Check required suffix if specified
if required_suffix and not url.endswith(required_suffix):
raise ValueError(f"URL should end with {required_suffix}")
return url
def validate_project_name(project: str, default_name: str) -> str:
"""
Validate and normalize project name
Args:
project: Project name to validate
default_name: Default name to use if input is None or empty
Returns:
Normalized project name
"""
if not project or project.strip() == "":
return default_name
return project.strip()

@ -1,4 +1,5 @@
import json import json
import uuid
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import Any, Optional, cast from typing import Any, Optional, cast
@ -102,6 +103,36 @@ class AgentNode(ToolNode):
try: try:
# convert tool messages # convert tool messages
agent_thoughts: list = []
from core.tools.entities.tool_entities import ToolInvokeMessage
thought_log_message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.LOG,
message=ToolInvokeMessage.LogMessage(
id=str(uuid.uuid4()),
label=f"Agent Strategy: {cast(AgentNodeData, self.node_data).agent_strategy_name}",
parent_id=None,
error=None,
status=ToolInvokeMessage.LogMessage.LogStatus.START,
data={
"strategy": cast(AgentNodeData, self.node_data).agent_strategy_name,
"parameters": parameters_for_log,
"thought_process": "Agent strategy execution started",
},
metadata={
"icon": self.agent_strategy_icon,
"agent_strategy": cast(AgentNodeData, self.node_data).agent_strategy_name,
},
),
)
from core.tools.entities.tool_entities import ToolInvokeMessage
def enhanced_message_stream():
yield thought_log_message
yield from message_stream
yield from self._transform_message( yield from self._transform_message(
message_stream, message_stream,
@ -110,6 +141,7 @@ class AgentNode(ToolNode):
"agent_strategy": cast(AgentNodeData, self.node_data).agent_strategy_name, "agent_strategy": cast(AgentNodeData, self.node_data).agent_strategy_name,
}, },
parameters_for_log, parameters_for_log,
agent_thoughts,
) )
except PluginDaemonClientSideError as e: except PluginDaemonClientSideError as e:
yield RunCompletedEvent( yield RunCompletedEvent(

@ -8,6 +8,7 @@ from typing import Any, Literal
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
import httpx import httpx
from json_repair import repair_json
from configs import dify_config from configs import dify_config
from core.file import file_manager from core.file import file_manager
@ -178,7 +179,8 @@ class Executor:
raise RequestBodyError("json body type should have exactly one item") raise RequestBodyError("json body type should have exactly one item")
json_string = self.variable_pool.convert_template(data[0].value).text json_string = self.variable_pool.convert_template(data[0].value).text
try: try:
json_object = json.loads(json_string, strict=False) repaired = repair_json(json_string)
json_object = json.loads(repaired, strict=False)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise RequestBodyError(f"Failed to parse JSON: {json_string}") from e raise RequestBodyError(f"Failed to parse JSON: {json_string}") from e
self.json = json_object self.json = json_object

@ -1,5 +1,5 @@
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import Any, cast from typing import Any, Optional, cast
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -191,6 +191,7 @@ class ToolNode(BaseNode[ToolNodeData]):
messages: Generator[ToolInvokeMessage, None, None], messages: Generator[ToolInvokeMessage, None, None],
tool_info: Mapping[str, Any], tool_info: Mapping[str, Any],
parameters_for_log: dict[str, Any], parameters_for_log: dict[str, Any],
agent_thoughts: Optional[list] = None,
) -> Generator: ) -> Generator:
""" """
Convert ToolInvokeMessages into tuple[plain_text, files] Convert ToolInvokeMessages into tuple[plain_text, files]
@ -368,11 +369,35 @@ class ToolNode(BaseNode[ToolNodeData]):
agent_logs.append(agent_log) agent_logs.append(agent_log)
yield agent_log yield agent_log
# Add agent_logs to outputs['json'] to ensure frontend can access thinking process
json_output: dict[str, Any] = {}
if json:
if isinstance(json, list) and len(json) == 1:
# If json is a list with only one element, convert it to a dictionary
json_output = json[0] if isinstance(json[0], dict) else {"data": json[0]}
elif isinstance(json, list):
# If json is a list with multiple elements, create a dictionary containing all data
json_output = {"data": json}
if agent_logs:
# Add agent_logs to json output
json_output["agent_logs"] = [
{
"id": log.id,
"parent_id": log.parent_id,
"error": log.error,
"status": log.status,
"data": log.data,
"label": log.label,
"metadata": log.metadata,
"node_id": log.node_id,
}
for log in agent_logs
]
yield RunCompletedEvent( yield RunCompletedEvent(
run_result=NodeRunResult( run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED, status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={"text": text, "files": ArrayFileSegment(value=files), "json": json, **variables}, outputs={"text": text, "files": ArrayFileSegment(value=files), "json": json_output, **variables},
metadata={ metadata={
**agent_execution_metadata, **agent_execution_metadata,
WorkflowNodeExecutionMetadataKey.TOOL_INFO: tool_info, WorkflowNodeExecutionMetadataKey.TOOL_INFO: tool_info,

@ -94,6 +94,16 @@ class OpsService:
new_decrypt_tracing_config.update({"project_url": project_url}) new_decrypt_tracing_config.update({"project_url": project_url})
except Exception: except Exception:
new_decrypt_tracing_config.update({"project_url": "https://wandb.ai/"}) new_decrypt_tracing_config.update({"project_url": "https://wandb.ai/"})
if tracing_provider == "aliyun" and (
"project_url" not in decrypt_tracing_config or not decrypt_tracing_config.get("project_url")
):
try:
project_url = OpsTraceManager.get_trace_config_project_url(decrypt_tracing_config, tracing_provider)
new_decrypt_tracing_config.update({"project_url": project_url})
except Exception:
new_decrypt_tracing_config.update({"project_url": "https://arms.console.aliyun.com/"})
trace_config_data.tracing_config = new_decrypt_tracing_config trace_config_data.tracing_config = new_decrypt_tracing_config
return trace_config_data.to_dict() return trace_config_data.to_dict()

@ -1,6 +1,7 @@
import os import os
from flask import Flask from flask import Flask
from packaging.version import Version
from yarl import URL from yarl import URL
from configs.app_config import DifyConfig from configs.app_config import DifyConfig
@ -40,6 +41,9 @@ def test_dify_config(monkeypatch):
assert config.WORKFLOW_PARALLEL_DEPTH_LIMIT == 3 assert config.WORKFLOW_PARALLEL_DEPTH_LIMIT == 3
# values from pyproject.toml
assert Version(config.project.version) >= Version("1.0.0")
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected. # NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`. # This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.

@ -0,0 +1 @@
# Unit tests for core ops module

@ -0,0 +1,309 @@
import pytest
from pydantic import ValidationError
from core.ops.entities.config_entity import (
ArizeConfig,
LangfuseConfig,
LangSmithConfig,
OpikConfig,
PhoenixConfig,
TracingProviderEnum,
WeaveConfig,
)
class TestTracingProviderEnum:
"""Test cases for TracingProviderEnum"""
def test_enum_values(self):
"""Test that all expected enum values are present"""
assert TracingProviderEnum.ARIZE == "arize"
assert TracingProviderEnum.PHOENIX == "phoenix"
assert TracingProviderEnum.LANGFUSE == "langfuse"
assert TracingProviderEnum.LANGSMITH == "langsmith"
assert TracingProviderEnum.OPIK == "opik"
assert TracingProviderEnum.WEAVE == "weave"
class TestArizeConfig:
"""Test cases for ArizeConfig"""
def test_valid_config(self):
"""Test valid Arize configuration"""
config = ArizeConfig(
api_key="test_key", space_id="test_space", project="test_project", endpoint="https://custom.arize.com"
)
assert config.api_key == "test_key"
assert config.space_id == "test_space"
assert config.project == "test_project"
assert config.endpoint == "https://custom.arize.com"
def test_default_values(self):
"""Test default values are set correctly"""
config = ArizeConfig()
assert config.api_key is None
assert config.space_id is None
assert config.project is None
assert config.endpoint == "https://otlp.arize.com"
def test_project_validation_empty(self):
"""Test project validation with empty value"""
config = ArizeConfig(project="")
assert config.project == "default"
def test_project_validation_none(self):
"""Test project validation with None value"""
config = ArizeConfig(project=None)
assert config.project == "default"
def test_endpoint_validation_empty(self):
"""Test endpoint validation with empty value"""
config = ArizeConfig(endpoint="")
assert config.endpoint == "https://otlp.arize.com"
def test_endpoint_validation_with_path(self):
"""Test endpoint validation normalizes URL by removing path"""
config = ArizeConfig(endpoint="https://custom.arize.com/api/v1")
assert config.endpoint == "https://custom.arize.com"
def test_endpoint_validation_invalid_scheme(self):
"""Test endpoint validation rejects invalid schemes"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
ArizeConfig(endpoint="ftp://invalid.com")
def test_endpoint_validation_no_scheme(self):
"""Test endpoint validation rejects URLs without scheme"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
ArizeConfig(endpoint="invalid.com")
class TestPhoenixConfig:
"""Test cases for PhoenixConfig"""
def test_valid_config(self):
"""Test valid Phoenix configuration"""
config = PhoenixConfig(api_key="test_key", project="test_project", endpoint="https://custom.phoenix.com")
assert config.api_key == "test_key"
assert config.project == "test_project"
assert config.endpoint == "https://custom.phoenix.com"
def test_default_values(self):
"""Test default values are set correctly"""
config = PhoenixConfig()
assert config.api_key is None
assert config.project is None
assert config.endpoint == "https://app.phoenix.arize.com"
def test_project_validation_empty(self):
"""Test project validation with empty value"""
config = PhoenixConfig(project="")
assert config.project == "default"
def test_endpoint_validation_with_path(self):
"""Test endpoint validation normalizes URL by removing path"""
config = PhoenixConfig(endpoint="https://custom.phoenix.com/api/v1")
assert config.endpoint == "https://custom.phoenix.com"
class TestLangfuseConfig:
"""Test cases for LangfuseConfig"""
def test_valid_config(self):
"""Test valid Langfuse configuration"""
config = LangfuseConfig(public_key="public_key", secret_key="secret_key", host="https://custom.langfuse.com")
assert config.public_key == "public_key"
assert config.secret_key == "secret_key"
assert config.host == "https://custom.langfuse.com"
def test_default_values(self):
"""Test default values are set correctly"""
config = LangfuseConfig(public_key="public", secret_key="secret")
assert config.host == "https://api.langfuse.com"
def test_missing_required_fields(self):
"""Test that required fields are enforced"""
with pytest.raises(ValidationError):
LangfuseConfig()
with pytest.raises(ValidationError):
LangfuseConfig(public_key="public")
with pytest.raises(ValidationError):
LangfuseConfig(secret_key="secret")
def test_host_validation_empty(self):
"""Test host validation with empty value"""
config = LangfuseConfig(public_key="public", secret_key="secret", host="")
assert config.host == "https://api.langfuse.com"
class TestLangSmithConfig:
"""Test cases for LangSmithConfig"""
def test_valid_config(self):
"""Test valid LangSmith configuration"""
config = LangSmithConfig(api_key="test_key", project="test_project", endpoint="https://custom.smith.com")
assert config.api_key == "test_key"
assert config.project == "test_project"
assert config.endpoint == "https://custom.smith.com"
def test_default_values(self):
"""Test default values are set correctly"""
config = LangSmithConfig(api_key="key", project="project")
assert config.endpoint == "https://api.smith.langchain.com"
def test_missing_required_fields(self):
"""Test that required fields are enforced"""
with pytest.raises(ValidationError):
LangSmithConfig()
with pytest.raises(ValidationError):
LangSmithConfig(api_key="key")
with pytest.raises(ValidationError):
LangSmithConfig(project="project")
def test_endpoint_validation_https_only(self):
"""Test endpoint validation only allows HTTPS"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
LangSmithConfig(api_key="key", project="project", endpoint="http://insecure.com")
class TestOpikConfig:
"""Test cases for OpikConfig"""
def test_valid_config(self):
"""Test valid Opik configuration"""
config = OpikConfig(
api_key="test_key",
project="test_project",
workspace="test_workspace",
url="https://custom.comet.com/opik/api/",
)
assert config.api_key == "test_key"
assert config.project == "test_project"
assert config.workspace == "test_workspace"
assert config.url == "https://custom.comet.com/opik/api/"
def test_default_values(self):
"""Test default values are set correctly"""
config = OpikConfig()
assert config.api_key is None
assert config.project is None
assert config.workspace is None
assert config.url == "https://www.comet.com/opik/api/"
def test_project_validation_empty(self):
"""Test project validation with empty value"""
config = OpikConfig(project="")
assert config.project == "Default Project"
def test_url_validation_empty(self):
"""Test URL validation with empty value"""
config = OpikConfig(url="")
assert config.url == "https://www.comet.com/opik/api/"
def test_url_validation_missing_suffix(self):
"""Test URL validation requires /api/ suffix"""
with pytest.raises(ValidationError, match="URL should end with /api/"):
OpikConfig(url="https://custom.comet.com/opik/")
def test_url_validation_invalid_scheme(self):
"""Test URL validation rejects invalid schemes"""
with pytest.raises(ValidationError, match="URL must start with https:// or http://"):
OpikConfig(url="ftp://custom.comet.com/opik/api/")
class TestWeaveConfig:
"""Test cases for WeaveConfig"""
def test_valid_config(self):
"""Test valid Weave configuration"""
config = WeaveConfig(
api_key="test_key",
entity="test_entity",
project="test_project",
endpoint="https://custom.wandb.ai",
host="https://custom.host.com",
)
assert config.api_key == "test_key"
assert config.entity == "test_entity"
assert config.project == "test_project"
assert config.endpoint == "https://custom.wandb.ai"
assert config.host == "https://custom.host.com"
def test_default_values(self):
"""Test default values are set correctly"""
config = WeaveConfig(api_key="key", project="project")
assert config.entity is None
assert config.endpoint == "https://trace.wandb.ai"
assert config.host is None
def test_missing_required_fields(self):
"""Test that required fields are enforced"""
with pytest.raises(ValidationError):
WeaveConfig()
with pytest.raises(ValidationError):
WeaveConfig(api_key="key")
with pytest.raises(ValidationError):
WeaveConfig(project="project")
def test_endpoint_validation_https_only(self):
"""Test endpoint validation only allows HTTPS"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
WeaveConfig(api_key="key", project="project", endpoint="http://insecure.wandb.ai")
def test_host_validation_optional(self):
"""Test host validation is optional but validates when provided"""
config = WeaveConfig(api_key="key", project="project", host=None)
assert config.host is None
config = WeaveConfig(api_key="key", project="project", host="")
assert config.host == ""
config = WeaveConfig(api_key="key", project="project", host="https://valid.host.com")
assert config.host == "https://valid.host.com"
def test_host_validation_invalid_scheme(self):
"""Test host validation rejects invalid schemes when provided"""
with pytest.raises(ValidationError, match="URL scheme must be one of"):
WeaveConfig(api_key="key", project="project", host="ftp://invalid.host.com")
class TestConfigIntegration:
"""Integration tests for configuration classes"""
def test_all_configs_can_be_instantiated(self):
"""Test that all config classes can be instantiated with valid data"""
configs = [
ArizeConfig(api_key="key"),
PhoenixConfig(api_key="key"),
LangfuseConfig(public_key="public", secret_key="secret"),
LangSmithConfig(api_key="key", project="project"),
OpikConfig(api_key="key"),
WeaveConfig(api_key="key", project="project"),
]
for config in configs:
assert config is not None
def test_url_normalization_consistency(self):
"""Test that URL normalization works consistently across configs"""
# Test that paths are removed from endpoints
arize_config = ArizeConfig(endpoint="https://arize.com/api/v1/test")
phoenix_config = PhoenixConfig(endpoint="https://phoenix.com/api/v2/")
assert arize_config.endpoint == "https://arize.com"
assert phoenix_config.endpoint == "https://phoenix.com"
def test_project_default_values(self):
"""Test that project default values are set correctly"""
arize_config = ArizeConfig(project="")
phoenix_config = PhoenixConfig(project="")
opik_config = OpikConfig(project="")
assert arize_config.project == "default"
assert phoenix_config.project == "default"
assert opik_config.project == "Default Project"

@ -0,0 +1,138 @@
import pytest
from core.ops.utils import validate_project_name, validate_url, validate_url_with_path
class TestValidateUrl:
"""Test cases for validate_url function"""
def test_valid_https_url(self):
"""Test valid HTTPS URL"""
result = validate_url("https://example.com", "https://default.com")
assert result == "https://example.com"
def test_valid_http_url(self):
"""Test valid HTTP URL"""
result = validate_url("http://example.com", "https://default.com")
assert result == "http://example.com"
def test_url_with_path_removed(self):
"""Test that URL path is removed during normalization"""
result = validate_url("https://example.com/api/v1/test", "https://default.com")
assert result == "https://example.com"
def test_url_with_query_removed(self):
"""Test that URL query parameters are removed"""
result = validate_url("https://example.com?param=value", "https://default.com")
assert result == "https://example.com"
def test_url_with_fragment_removed(self):
"""Test that URL fragments are removed"""
result = validate_url("https://example.com#section", "https://default.com")
assert result == "https://example.com"
def test_empty_url_returns_default(self):
"""Test empty URL returns default"""
result = validate_url("", "https://default.com")
assert result == "https://default.com"
def test_none_url_returns_default(self):
"""Test None URL returns default"""
result = validate_url(None, "https://default.com")
assert result == "https://default.com"
def test_whitespace_url_returns_default(self):
"""Test whitespace URL returns default"""
result = validate_url(" ", "https://default.com")
assert result == "https://default.com"
def test_invalid_scheme_raises_error(self):
"""Test invalid scheme raises ValueError"""
with pytest.raises(ValueError, match="URL scheme must be one of"):
validate_url("ftp://example.com", "https://default.com")
def test_no_scheme_raises_error(self):
"""Test URL without scheme raises ValueError"""
with pytest.raises(ValueError, match="URL scheme must be one of"):
validate_url("example.com", "https://default.com")
def test_custom_allowed_schemes(self):
"""Test custom allowed schemes"""
result = validate_url("https://example.com", "https://default.com", allowed_schemes=("https",))
assert result == "https://example.com"
with pytest.raises(ValueError, match="URL scheme must be one of"):
validate_url("http://example.com", "https://default.com", allowed_schemes=("https",))
class TestValidateUrlWithPath:
"""Test cases for validate_url_with_path function"""
def test_valid_url_with_path(self):
"""Test valid URL with path"""
result = validate_url_with_path("https://example.com/api/v1", "https://default.com")
assert result == "https://example.com/api/v1"
def test_valid_url_with_required_suffix(self):
"""Test valid URL with required suffix"""
result = validate_url_with_path("https://example.com/api/", "https://default.com", required_suffix="/api/")
assert result == "https://example.com/api/"
def test_url_without_required_suffix_raises_error(self):
"""Test URL without required suffix raises error"""
with pytest.raises(ValueError, match="URL should end with /api/"):
validate_url_with_path("https://example.com/api", "https://default.com", required_suffix="/api/")
def test_empty_url_returns_default(self):
"""Test empty URL returns default"""
result = validate_url_with_path("", "https://default.com")
assert result == "https://default.com"
def test_none_url_returns_default(self):
"""Test None URL returns default"""
result = validate_url_with_path(None, "https://default.com")
assert result == "https://default.com"
def test_invalid_scheme_raises_error(self):
"""Test invalid scheme raises ValueError"""
with pytest.raises(ValueError, match="URL must start with https:// or http://"):
validate_url_with_path("ftp://example.com", "https://default.com")
def test_no_scheme_raises_error(self):
"""Test URL without scheme raises ValueError"""
with pytest.raises(ValueError, match="URL must start with https:// or http://"):
validate_url_with_path("example.com", "https://default.com")
class TestValidateProjectName:
"""Test cases for validate_project_name function"""
def test_valid_project_name(self):
"""Test valid project name"""
result = validate_project_name("my-project", "default")
assert result == "my-project"
def test_empty_project_name_returns_default(self):
"""Test empty project name returns default"""
result = validate_project_name("", "default")
assert result == "default"
def test_none_project_name_returns_default(self):
"""Test None project name returns default"""
result = validate_project_name(None, "default")
assert result == "default"
def test_whitespace_project_name_returns_default(self):
"""Test whitespace project name returns default"""
result = validate_project_name(" ", "default")
assert result == "default"
def test_project_name_with_whitespace_trimmed(self):
"""Test project name with whitespace is trimmed"""
result = validate_project_name(" my-project ", "default")
assert result == "my-project"
def test_custom_default_name(self):
"""Test custom default name"""
result = validate_project_name("", "Custom Default")
assert result == "Custom Default"

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import TracingIcon from './tracing-icon' import TracingIcon from './tracing-icon'
import ProviderPanel from './provider-panel' import ProviderPanel from './provider-panel'
import type { ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type'
import { TracingProvider } from './type' import { TracingProvider } from './type'
import ProviderConfigModal from './provider-config-modal' import ProviderConfigModal from './provider-config-modal'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
@ -29,7 +29,8 @@ export type PopupProps = {
langFuseConfig: LangFuseConfig | null langFuseConfig: LangFuseConfig | null
opikConfig: OpikConfig | null opikConfig: OpikConfig | null
weaveConfig: WeaveConfig | null weaveConfig: WeaveConfig | null
onConfigUpdated: (provider: TracingProvider, payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void aliyunConfig: AliyunConfig | null
onConfigUpdated: (provider: TracingProvider, payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => void
onConfigRemoved: (provider: TracingProvider) => void onConfigRemoved: (provider: TracingProvider) => void
} }
@ -46,6 +47,7 @@ const ConfigPopup: FC<PopupProps> = ({
langFuseConfig, langFuseConfig,
opikConfig, opikConfig,
weaveConfig, weaveConfig,
aliyunConfig,
onConfigUpdated, onConfigUpdated,
onConfigRemoved, onConfigRemoved,
}) => { }) => {
@ -69,7 +71,7 @@ const ConfigPopup: FC<PopupProps> = ({
} }
}, [onChooseProvider]) }, [onChooseProvider])
const handleConfigUpdated = useCallback((payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => { const handleConfigUpdated = useCallback((payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => {
onConfigUpdated(currentProvider!, payload) onConfigUpdated(currentProvider!, payload)
hideConfigModal() hideConfigModal()
}, [currentProvider, hideConfigModal, onConfigUpdated]) }, [currentProvider, hideConfigModal, onConfigUpdated])
@ -79,8 +81,8 @@ const ConfigPopup: FC<PopupProps> = ({
hideConfigModal() hideConfigModal()
}, [currentProvider, hideConfigModal, onConfigRemoved]) }, [currentProvider, hideConfigModal, onConfigRemoved])
const providerAllConfigured = arizeConfig && phoenixConfig && langSmithConfig && langFuseConfig && opikConfig && weaveConfig const providerAllConfigured = arizeConfig && phoenixConfig && langSmithConfig && langFuseConfig && opikConfig && weaveConfig && aliyunConfig
const providerAllNotConfigured = !arizeConfig && !phoenixConfig && !langSmithConfig && !langFuseConfig && !opikConfig && !weaveConfig const providerAllNotConfigured = !arizeConfig && !phoenixConfig && !langSmithConfig && !langFuseConfig && !opikConfig && !weaveConfig && !aliyunConfig
const switchContent = ( const switchContent = (
<Switch <Switch
@ -167,6 +169,19 @@ const ConfigPopup: FC<PopupProps> = ({
key="weave-provider-panel" key="weave-provider-panel"
/> />
) )
const aliyunPanel = (
<ProviderPanel
type={TracingProvider.aliyun}
readOnly={readOnly}
config={aliyunConfig}
hasConfigured={!!aliyunConfig}
onConfig={handleOnConfig(TracingProvider.aliyun)}
isChosen={chosenProvider === TracingProvider.aliyun}
onChoose={handleOnChoose(TracingProvider.aliyun)}
key="alyun-provider-panel"
/>
)
const configuredProviderPanel = () => { const configuredProviderPanel = () => {
const configuredPanels: JSX.Element[] = [] const configuredPanels: JSX.Element[] = []
@ -188,6 +203,9 @@ const ConfigPopup: FC<PopupProps> = ({
if (phoenixConfig) if (phoenixConfig)
configuredPanels.push(phoenixPanel) configuredPanels.push(phoenixPanel)
if (aliyunConfig)
configuredPanels.push(aliyunPanel)
return configuredPanels return configuredPanels
} }
@ -212,6 +230,9 @@ const ConfigPopup: FC<PopupProps> = ({
if (!weaveConfig) if (!weaveConfig)
notConfiguredPanels.push(weavePanel) notConfiguredPanels.push(weavePanel)
if (!aliyunConfig)
notConfiguredPanels.push(aliyunPanel)
return notConfiguredPanels return notConfiguredPanels
} }
@ -226,6 +247,8 @@ const ConfigPopup: FC<PopupProps> = ({
return langFuseConfig return langFuseConfig
if (currentProvider === TracingProvider.opik) if (currentProvider === TracingProvider.opik)
return opikConfig return opikConfig
if (currentProvider === TracingProvider.aliyun)
return aliyunConfig
return weaveConfig return weaveConfig
} }
@ -273,6 +296,7 @@ const ConfigPopup: FC<PopupProps> = ({
{weavePanel} {weavePanel}
{arizePanel} {arizePanel}
{phoenixPanel} {phoenixPanel}
{aliyunPanel}
</div> </div>
</> </>
) )

@ -7,4 +7,5 @@ export const docURL = {
[TracingProvider.langfuse]: 'https://docs.langfuse.com', [TracingProvider.langfuse]: 'https://docs.langfuse.com',
[TracingProvider.opik]: 'https://www.comet.com/docs/opik/tracing/integrations/dify#setup-instructions', [TracingProvider.opik]: 'https://www.comet.com/docs/opik/tracing/integrations/dify#setup-instructions',
[TracingProvider.weave]: 'https://weave-docs.wandb.ai/', [TracingProvider.weave]: 'https://weave-docs.wandb.ai/',
[TracingProvider.aliyun]: 'https://help.aliyun.com/zh/arms/tracing-analysis/untitled-document-1750672984680',
} }

@ -7,12 +7,12 @@ import {
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import type { ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type'
import { TracingProvider } from './type' import { TracingProvider } from './type'
import TracingIcon from './tracing-icon' import TracingIcon from './tracing-icon'
import ConfigButton from './config-button' import ConfigButton from './config-button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { ArizeIcon, LangfuseIcon, LangsmithIcon, OpikIcon, PhoenixIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import { AliyunIcon, ArizeIcon, LangfuseIcon, LangsmithIcon, OpikIcon, PhoenixIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
import type { TracingStatus } from '@/models/app' import type { TracingStatus } from '@/models/app'
@ -69,6 +69,7 @@ const Panel: FC = () => {
[TracingProvider.langfuse]: LangfuseIcon, [TracingProvider.langfuse]: LangfuseIcon,
[TracingProvider.opik]: OpikIcon, [TracingProvider.opik]: OpikIcon,
[TracingProvider.weave]: WeaveIcon, [TracingProvider.weave]: WeaveIcon,
[TracingProvider.aliyun]: AliyunIcon,
} }
const InUseProviderIcon = inUseTracingProvider ? providerIconMap[inUseTracingProvider] : undefined const InUseProviderIcon = inUseTracingProvider ? providerIconMap[inUseTracingProvider] : undefined
@ -78,7 +79,8 @@ const Panel: FC = () => {
const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null) const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
const [opikConfig, setOpikConfig] = useState<OpikConfig | null>(null) const [opikConfig, setOpikConfig] = useState<OpikConfig | null>(null)
const [weaveConfig, setWeaveConfig] = useState<WeaveConfig | null>(null) const [weaveConfig, setWeaveConfig] = useState<WeaveConfig | null>(null)
const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig || arizeConfig || phoenixConfig) const [aliyunConfig, setAliyunConfig] = useState<AliyunConfig | null>(null)
const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig || arizeConfig || phoenixConfig || aliyunConfig)
const fetchTracingConfig = async () => { const fetchTracingConfig = async () => {
const { tracing_config: arizeConfig, has_not_configured: arizeHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.arize }) const { tracing_config: arizeConfig, has_not_configured: arizeHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.arize })
@ -99,6 +101,9 @@ const Panel: FC = () => {
const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave }) const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave })
if (!weaveHasNotConfig) if (!weaveHasNotConfig)
setWeaveConfig(weaveConfig as WeaveConfig) setWeaveConfig(weaveConfig as WeaveConfig)
const { tracing_config: aliyunConfig, has_not_configured: aliyunHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.aliyun })
if (!aliyunHasNotConfig)
setAliyunConfig(aliyunConfig as AliyunConfig)
} }
const handleTracingConfigUpdated = async (provider: TracingProvider) => { const handleTracingConfigUpdated = async (provider: TracingProvider) => {
@ -116,6 +121,8 @@ const Panel: FC = () => {
setOpikConfig(tracing_config as OpikConfig) setOpikConfig(tracing_config as OpikConfig)
else if (provider === TracingProvider.weave) else if (provider === TracingProvider.weave)
setWeaveConfig(tracing_config as WeaveConfig) setWeaveConfig(tracing_config as WeaveConfig)
else if (provider === TracingProvider.aliyun)
setAliyunConfig(tracing_config as AliyunConfig)
} }
const handleTracingConfigRemoved = (provider: TracingProvider) => { const handleTracingConfigRemoved = (provider: TracingProvider) => {
@ -131,6 +138,8 @@ const Panel: FC = () => {
setOpikConfig(null) setOpikConfig(null)
else if (provider === TracingProvider.weave) else if (provider === TracingProvider.weave)
setWeaveConfig(null) setWeaveConfig(null)
else if (provider === TracingProvider.aliyun)
setAliyunConfig(null)
if (provider === inUseTracingProvider) { if (provider === inUseTracingProvider) {
handleTracingStatusChange({ handleTracingStatusChange({
enabled: false, enabled: false,
@ -191,6 +200,7 @@ const Panel: FC = () => {
langFuseConfig={langFuseConfig} langFuseConfig={langFuseConfig}
opikConfig={opikConfig} opikConfig={opikConfig}
weaveConfig={weaveConfig} weaveConfig={weaveConfig}
aliyunConfig={aliyunConfig}
onConfigUpdated={handleTracingConfigUpdated} onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved} onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup} controlShowPopup={controlShowPopup}
@ -228,6 +238,7 @@ const Panel: FC = () => {
langFuseConfig={langFuseConfig} langFuseConfig={langFuseConfig}
opikConfig={opikConfig} opikConfig={opikConfig}
weaveConfig={weaveConfig} weaveConfig={weaveConfig}
aliyunConfig={aliyunConfig}
onConfigUpdated={handleTracingConfigUpdated} onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved} onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup} controlShowPopup={controlShowPopup}

@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import Field from './field' import Field from './field'
import type { ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type'
import { TracingProvider } from './type' import { TracingProvider } from './type'
import { docURL } from './config' import { docURL } from './config'
import { import {
@ -22,10 +22,10 @@ import Divider from '@/app/components/base/divider'
type Props = { type Props = {
appId: string appId: string
type: TracingProvider type: TracingProvider
payload?: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | null payload?: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig | null
onRemoved: () => void onRemoved: () => void
onCancel: () => void onCancel: () => void
onSaved: (payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void onSaved: (payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => void
onChosen: (provider: TracingProvider) => void onChosen: (provider: TracingProvider) => void
} }
@ -71,6 +71,12 @@ const weaveConfigTemplate = {
host: '', host: '',
} }
const aliyunConfigTemplate = {
app_name: '',
license_key: '',
endpoint: '',
}
const ProviderConfigModal: FC<Props> = ({ const ProviderConfigModal: FC<Props> = ({
appId, appId,
type, type,
@ -84,7 +90,7 @@ const ProviderConfigModal: FC<Props> = ({
const isEdit = !!payload const isEdit = !!payload
const isAdd = !isEdit const isAdd = !isEdit
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const [config, setConfig] = useState<ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig>((() => { const [config, setConfig] = useState<ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig>((() => {
if (isEdit) if (isEdit)
return payload return payload
@ -103,6 +109,9 @@ const ProviderConfigModal: FC<Props> = ({
else if (type === TracingProvider.opik) else if (type === TracingProvider.opik)
return opikConfigTemplate return opikConfigTemplate
else if (type === TracingProvider.aliyun)
return aliyunConfigTemplate
return weaveConfigTemplate return weaveConfigTemplate
})()) })())
const [isShowRemoveConfirm, { const [isShowRemoveConfirm, {
@ -183,6 +192,16 @@ const ProviderConfigModal: FC<Props> = ({
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) }) errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
} }
if (type === TracingProvider.aliyun) {
const postData = config as AliyunConfig
if (!errorMessage && !postData.app_name)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'App Name' })
if (!errorMessage && !postData.license_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'License Key' })
if (!errorMessage && !postData.endpoint)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' })
}
return errorMessage return errorMessage
}, [config, t, type]) }, [config, t, type])
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
@ -294,6 +313,31 @@ const ProviderConfigModal: FC<Props> = ({
/> />
</> </>
)} )}
{type === TracingProvider.aliyun && (
<>
<Field
label='License Key'
labelClassName='!text-sm'
isRequired
value={(config as AliyunConfig).license_key}
onChange={handleConfigChange('license_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as AliyunConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://tracing.arms.aliyuncs.com'}
/>
<Field
label='App Name'
labelClassName='!text-sm'
value={(config as AliyunConfig).app_name}
onChange={handleConfigChange('app_name')}
/>
</>
)}
{type === TracingProvider.weave && ( {type === TracingProvider.weave && (
<> <>
<Field <Field

@ -7,7 +7,7 @@ import {
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TracingProvider } from './type' import { TracingProvider } from './type'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { ArizeIconBig, LangfuseIconBig, LangsmithIconBig, OpikIconBig, PhoenixIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing' import { AliyunIconBig, ArizeIconBig, LangfuseIconBig, LangsmithIconBig, OpikIconBig, PhoenixIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing'
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
const I18N_PREFIX = 'app.tracing' const I18N_PREFIX = 'app.tracing'
@ -30,6 +30,7 @@ const getIcon = (type: TracingProvider) => {
[TracingProvider.langfuse]: LangfuseIconBig, [TracingProvider.langfuse]: LangfuseIconBig,
[TracingProvider.opik]: OpikIconBig, [TracingProvider.opik]: OpikIconBig,
[TracingProvider.weave]: WeaveIconBig, [TracingProvider.weave]: WeaveIconBig,
[TracingProvider.aliyun]: AliyunIconBig,
})[type] })[type]
} }

@ -5,6 +5,7 @@ export enum TracingProvider {
langfuse = 'langfuse', langfuse = 'langfuse',
opik = 'opik', opik = 'opik',
weave = 'weave', weave = 'weave',
aliyun = 'aliyun',
} }
export type ArizeConfig = { export type ArizeConfig = {
@ -46,3 +47,9 @@ export type WeaveConfig = {
endpoint: string endpoint: string
host: string host: string
} }
export type AliyunConfig = {
app_name: string
license_key: string
endpoint: string
}

@ -77,7 +77,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
</Tooltip> </Tooltip>
)} )}
{isMobile && <div className={classNames('uppercase text-xs text-text-tertiary font-medium pb-2 pt-4', 'flex items-center justify-center !px-0 gap-1')}> {isMobile && <div className={classNames('pb-2 pt-4 text-xs font-medium uppercase text-text-tertiary', 'flex items-center justify-center gap-1 !px-0')}>
{relatedAppsTotal || '--'} {relatedAppsTotal || '--'}
<PaperClipIcon className='h-4 w-4 text-text-secondary' /> <PaperClipIcon className='h-4 w-4 text-text-secondary' />
</div>} </div>}

@ -10,7 +10,6 @@ import {
RiFileCopy2Line, RiFileCopy2Line,
RiFileDownloadLine, RiFileDownloadLine,
RiFileUploadLine, RiFileUploadLine,
RiMoreLine,
} from '@remixicon/react' } from '@remixicon/react'
import AppIcon from '../base/app-icon' import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal' import SwitchAppModal from '../app/switch-app-modal'
@ -35,7 +34,8 @@ import ContentDialog from '@/app/components/base/content-dialog'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView' import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
import Divider from '../base/divider' import Divider from '../base/divider'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem' import type { Operation } from './app-operations'
import AppOperations from './app-operations'
export type IAppInfoProps = { export type IAppInfoProps = {
expand: boolean expand: boolean
@ -186,14 +186,58 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const { isCurrentWorkspaceEditor } = useAppContext() const { isCurrentWorkspaceEditor } = useAppContext()
const [showMore, setShowMore] = useState(false)
const handleTriggerMore = useCallback(() => {
setShowMore(true)
}, [setShowMore])
if (!appDetail) if (!appDetail)
return null return null
const operations = [
{
id: 'edit',
title: t('app.editApp'),
icon: <RiEditLine />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowEditModal(true)
},
},
{
id: 'duplicate',
title: t('app.duplicate'),
icon: <RiFileCopy2Line />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowDuplicateModal(true)
},
},
{
id: 'export',
title: t('app.export'),
icon: <RiFileDownloadLine />,
onClick: exportCheck,
},
(appDetail.mode !== 'agent-chat' && (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow')) ? {
id: 'import',
title: t('workflow.common.importDSL'),
icon: <RiFileUploadLine />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowImportDSLModal(true)
},
} : undefined,
(appDetail.mode !== 'agent-chat' && (appDetail.mode === 'completion' || appDetail.mode === 'chat')) ? {
id: 'switch',
title: t('app.switch'),
icon: <RiExchange2Line />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowSwitchModal(true)
},
} : undefined,
].filter((op): op is Operation => Boolean(op))
return ( return (
<div> <div>
{!onlyShowDetail && ( {!onlyShowDetail && (
@ -259,88 +303,10 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
<div className='system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary'>{appDetail.description}</div> <div className='system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary'>{appDetail.description}</div>
)} )}
{/* operations */} {/* operations */}
<div className='flex flex-wrap items-center gap-1 self-stretch'> <AppOperations
<Button gap={4}
size={'small'} operations={operations}
variant={'secondary'} />
className='gap-[1px]'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowEditModal(true)
}}
>
<RiEditLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.editApp')}</span>
</Button>
<Button
size={'small'}
variant={'secondary'}
className='gap-[1px]'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowDuplicateModal(true)
}}>
<RiFileCopy2Line className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.duplicate')}</span>
</Button>
<Button
size={'small'}
variant={'secondary'}
className='gap-[1px]'
onClick={exportCheck}
>
<RiFileDownloadLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.export')}</span>
</Button>
{appDetail.mode !== 'agent-chat' && <PortalToFollowElem
open={showMore}
onOpenChange={setShowMore}
placement='bottom-end'
offset={{
mainAxis: 4,
}}>
<PortalToFollowElemTrigger onClick={handleTriggerMore}>
<Button
size={'small'}
variant={'secondary'}
className='gap-[1px]'
>
<RiMoreLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[21]'>
<div className='flex w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'>
{
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow')
&& <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowImportDSLModal(true)
}}>
<RiFileUploadLine className='h-4 w-4 text-text-tertiary' />
<span className='system-md-regular text-text-secondary'>{t('workflow.common.importDSL')}</span>
</div>
}
{
(appDetail.mode === 'completion' || appDetail.mode === 'chat')
&& <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowSwitchModal(true)
}}>
<RiExchange2Line className='h-4 w-4 text-text-tertiary' />
<span className='system-md-regular text-text-secondary'>{t('app.switch')}</span>
</div>
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>}
</div>
</div> </div>
<div className='flex flex-1'> <div className='flex flex-1'>
<CardView <CardView

@ -0,0 +1,145 @@
import type { ReactElement } from 'react'
import { cloneElement, useCallback } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
import { RiMoreLine } from '@remixicon/react'
export type Operation = {
id: string; title: string; icon: ReactElement; onClick: () => void
}
const AppOperations = ({ operations, gap }: {
operations: Operation[]
gap: number
}) => {
const { t } = useTranslation()
const [visibleOpreations, setVisibleOperations] = useState<Operation[]>([])
const [moreOperations, setMoreOperations] = useState<Operation[]>([])
const [showMore, setShowMore] = useState(false)
const navRef = useRef<HTMLDivElement>(null)
const handleTriggerMore = useCallback(() => {
setShowMore(true)
}, [setShowMore])
useEffect(() => {
const moreElement = document.getElementById('more')
const navElement = document.getElementById('nav')
let width = 0
const containerWidth = navElement?.clientWidth ?? 0
const moreWidth = moreElement?.clientWidth ?? 0
if (containerWidth === 0 || moreWidth === 0) return
const updatedEntries: Record<string, boolean> = operations.reduce((pre, cur) => {
pre[cur.id] = false
return pre
}, {} as Record<string, boolean>)
const childrens = Array.from(navRef.current!.children).slice(0, -1)
for (let i = 0; i < childrens.length; i++) {
const child: any = childrens[i]
const id = child.dataset.targetid
if (!id) break
const childWidth = child.clientWidth
if (width + gap + childWidth + moreWidth <= containerWidth) {
updatedEntries[id] = true
width += gap + childWidth
}
else {
if (i === childrens.length - 1 && width + childWidth <= containerWidth)
updatedEntries[id] = true
else
updatedEntries[id] = false
break
}
}
setVisibleOperations(operations.filter(item => updatedEntries[item.id]))
setMoreOperations(operations.filter(item => !updatedEntries[item.id]))
}, [operations, gap])
return (
<>
{!visibleOpreations.length && <div
id="nav"
ref={navRef}
className="flex h-0 items-center self-stretch overflow-hidden"
style={{ gap }}
>
{operations.map((operation, index) =>
<Button
key={index}
data-targetid={operation.id}
size={'small'}
variant={'secondary'}
className="gap-[1px]">
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
<span className="system-xs-medium text-components-button-secondary-text">
{operation.title}
</span>
</Button>,
)}
<Button
id="more"
size={'small'}
variant={'secondary'}
className="gap-[1px]"
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<span className="system-xs-medium text-components-button-secondary-text">
{t('common.operation.more')}
</span>
</Button>
</div>}
<div className="flex items-center self-stretch overflow-hidden" style={{ gap }}>
{visibleOpreations.map(operation =>
<Button
key={operation.id}
data-targetid={operation.id}
size={'small'}
variant={'secondary'}
className="gap-[1px]"
onClick={operation.onClick}>
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
<span className="system-xs-medium text-components-button-secondary-text">
{operation.title}
</span>
</Button>,
)}
{visibleOpreations.length < operations.length && <PortalToFollowElem
open={showMore}
onOpenChange={setShowMore}
placement='bottom-end'
offset={{
mainAxis: 4,
}}>
<PortalToFollowElemTrigger onClick={handleTriggerMore}>
<Button
size={'small'}
variant={'secondary'}
className='gap-[1px]'
>
<RiMoreLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[21]'>
<div className='flex min-w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'>
{moreOperations.map(item => <div
key={item.id}
className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
onClick={item.onClick}
>
{cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
<span className='system-md-regular text-text-secondary'>{item.title}</span>
</div>)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>}
</div>
</>
)
}
export default AppOperations

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1,118 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
"fill": "none",
"version": "1.1",
"width": "65",
"height": "16",
"viewBox": "0 0 65 16"
},
"children": [
{
"type": "element",
"name": "defs",
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "master_svg0_42_34281"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"x": "0",
"y": "0",
"width": "19",
"height": "16",
"rx": "0"
}
}
]
}
]
},
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#master_svg0_42_34281)"
},
"children": [
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.06862,14.6667C3.79213,14.6667,3.45463,14.5688,3.05614,14.373C2.97908,14.3351,2.92692,14.3105,2.89968,14.2992C2.33193,14.0628,1.82911,13.7294,1.39123,13.2989C0.463742,12.3871,0,11.2874,0,10C0,8.71258,0.463742,7.61293,1.39123,6.70107C2.16172,5.94358,3.06404,5.50073,4.09819,5.37252C4.23172,3.98276,4.81755,2.77756,5.85569,1.75693C7.04708,0.585642,8.4857,0,10.1716,0C11.5256,0,12.743,0.396982,13.8239,1.19095C14.8847,1.97019,15.61,2.97855,16,4.21604L14.7045,4.61063C14.4016,3.64918,13.8374,2.86532,13.0121,2.25905C12.1719,1.64191,11.2251,1.33333,10.1716,1.33333C8.8602,1.33333,7.74124,1.7888,6.81467,2.69974C5.88811,3.61067,5.42483,4.71076,5.42483,6L5.42483,6.66667L4.74673,6.66667C3.81172,6.66667,3.01288,6.99242,2.35021,7.64393C1.68754,8.2954,1.35621,9.08076,1.35621,10C1.35621,10.9192,1.68754,11.7046,2.35021,12.3561C2.66354,12.6641,3.02298,12.9026,3.42852,13.0714C3.48193,13.0937,3.55988,13.13,3.66237,13.1803C3.87004,13.2823,4.00545,13.3333,4.06862,13.3333L4.06862,14.6667Z",
"fill-rule": "evenodd",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
},
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.458613505859375,7.779393492279053C12.975613505859375,7.717463492279053,12.484813505859375,7.686503492279053,11.993983505859376,7.686503492279053C11.152583505859376,7.686503492279053,10.303403505859375,7.779393492279053,9.493183505859374,7.941943492279053C8.682953505859375,8.104503492279052,7.903893505859375,8.359943492279053,7.155983505859375,8.654083492279053C6.657383505859375,8.870823492279053,6.158783505859375,9.128843492279053,5.660181505859375,9.428153492279053C5.332974751859375,9.621673492279053,5.239486705859375,10.070633492279054,5.434253505859375,10.395743492279053L7.413073505859375,13.298533492279052C7.639003505859375,13.623603492279052,8.090863505859375,13.716463492279052,8.418073505859375,13.523003492279052C8.547913505859375,13.435263492279052,8.763453505859374,13.326893492279053,9.064693505859374,13.197863492279053C9.516553505859374,13.004333492279052,9.976203505859374,12.872733492279053,10.459223505859375,12.779863492279052C10.942243505859375,12.679263492279052,11.433053505859375,12.617333492279052,11.955023505859375,12.617333492279052L13.380683505859375,7.810353492279052L13.458613505859375,7.779393492279053ZM15.273813505859374,8.135463492279053L15.016753505859375,5.333333492279053L13.458613505859375,7.787133492279053C13.817013505859375,7.818093492279052,14.144213505859375,7.880023492279053,14.494743505859375,7.949683492279053C14.494743505859375,7.944523492279053,14.754433505859375,8.006453492279054,15.273813505859374,8.135463492279053ZM12.064083505859376,12.648273492279053L11.378523505859375,14.970463492279054L12.515943505859376,16.00003349227905L14.074083505859376,15.643933492279054L14.525943505859376,13.027603492279052C14.198743505859374,12.934663492279054,13.879283505859375,12.834063492279054,13.552083505859375,12.772133492279053C13.069083505859375,12.717933492279052,12.578283505859375,12.648273492279053,12.064083505859376,12.648273492279053ZM18.327743505859374,9.428153492279053C17.829143505859374,9.128843492279053,17.330543505859374,8.870823492279053,16.831943505859375,8.654083492279053C16.348943505859374,8.460573492279053,15.826943505859376,8.267053492279054,15.305013505859375,8.135463492279053L15.305013505859375,8.267053492279054L14.463613505859374,13.043063492279053C14.596083505859376,13.105003492279053,14.759683505859375,13.135933492279053,14.884283505859376,13.205603492279053C15.185523505859376,13.334623492279052,15.401043505859375,13.443003492279052,15.530943505859375,13.530733492279053C15.858143505859376,13.724263492279054,16.341143505859375,13.623603492279052,16.535943505859375,13.306263492279053L18.514743505859375,10.403483492279053C18.779643505859376,10.039673492279054,18.686143505859377,9.621673492279053,18.327743505859374,9.428153492279053Z",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
}
]
}
]
},
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M25.044,2.668L34.676,2.668L34.676,4.04L25.044,4.04L25.044,2.668ZM29.958,7.82Q29.258,9.066,28.355,10.41Q27.451999999999998,11.754,26.92,12.3L32.506,11.782Q31.442,10.158,30.84,9.346L32.058,8.562000000000001Q32.786,9.5,33.843,11.012Q34.9,12.524,35.516,13.546L34.214,14.526Q33.891999999999996,13.966,33.346000000000004,13.098Q32.016,13.182,29.734,13.378Q27.451999999999998,13.574,25.87,13.742L25.31,13.812L24.834,13.882L24.414,12.468Q24.708,12.37,24.862000000000002,12.265Q25.016,12.16,25.121,12.069Q25.226,11.978,25.268,11.936Q25.912,11.32,26.724,10.165Q27.536,9.01,28.208,7.82L23.854,7.82L23.854,6.434L35.866,6.434L35.866,7.82L29.958,7.82ZM42.656,7.414L42.656,8.576L41.354,8.576L41.354,1.814L42.656,1.87L42.656,7.036Q43.314,5.846,43.888000000000005,4.369Q44.462,2.892,44.714,1.6600000000000001L46.086,1.981999Q45.96,2.612,45.722,3.41L49.6,3.41L49.6,4.74L45.274,4.74Q44.616,6.56,43.706,8.128L42.656,7.414ZM38.596000000000004,2.346L39.884,2.402L39.884,8.212L38.596000000000004,8.212L38.596000000000004,2.346ZM46.184,4.964Q46.688,5.356,47.5,6.175Q48.312,6.994,48.788,7.582L47.751999999999995,8.59Q47.346000000000004,8.072,46.576,7.274Q45.806,6.476,45.204,5.902L46.184,4.964ZM48.41,9.01L48.41,12.706L49.894,12.706L49.894,13.966L37.391999999999996,13.966L37.391999999999996,12.706L38.848,12.706L38.848,9.01L48.41,9.01ZM41.676,10.256L40.164,10.256L40.164,12.706L41.676,12.706L41.676,10.256ZM42.908,12.706L44.364000000000004,12.706L44.364000000000004,10.256L42.908,10.256L42.908,12.706ZM45.582,12.706L47.108000000000004,12.706L47.108000000000004,10.256L45.582,10.256L45.582,12.706ZM54.906,7.456L55.116,8.394L54.178,8.814L54.178,12.818Q54.178,13.434,54.031,13.735Q53.884,14.036,53.534,14.162Q53.184,14.288,52.456,14.358L51.867999999999995,14.414L51.476,13.084L52.162,13.028Q52.512,13,52.652,12.958Q52.792,12.916,52.841,12.797Q52.89,12.678,52.89,12.384L52.89,9.36Q51.980000000000004,9.724,51.322,9.948L51.013999999999996,8.576Q51.798,8.324,52.89,7.876L52.89,5.524L51.42,5.524L51.42,4.166L52.89,4.166L52.89,1.7579989999999999L54.178,1.814L54.178,4.166L55.214,4.166L55.214,5.524L54.178,5.524L54.178,7.316L54.808,7.022L54.906,7.456ZM56.894,4.5440000000000005L56.894,6.098L55.564,6.098L55.564,3.256L58.686,3.256Q58.42,2.346,58.266,1.9260000000000002L59.624,1.7579989999999999Q59.848,2.276,60.142,3.256L63.25,3.256L63.25,6.098L61.962,6.098L61.962,4.5440000000000005L56.894,4.5440000000000005ZM59.008,6.322Q58.392,6.938,57.685,7.512Q56.978,8.086,55.956,8.841999999999999L55.242,7.764Q56.824,6.728,58.126,5.37L59.008,6.322ZM60.422,5.37Q61.024,5.776,62.095,6.581Q63.166,7.386,63.656,7.806L62.942,8.982Q62.368,8.45,61.332,7.652Q60.296,6.854,59.666,6.434L60.422,5.37ZM62.592,10.256L60.044,10.256L60.044,12.566L63.572,12.566L63.572,13.826L55.144,13.826L55.144,12.566L58.63,12.566L58.63,10.256L56.054,10.256L56.054,8.982L62.592,8.982L62.592,10.256Z",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
}
]
}
]
}
]
},
"name": "AliyunIcon"
}

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AliyunIcon.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'AliyunIcon'
export default Icon

@ -0,0 +1,71 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
"fill": "none",
"version": "1.1",
"width": "96",
"height": "24",
"viewBox": "0 0 96 24"
},
"children": [
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6.10294,22C5.68819,22,5.18195,21.8532,4.58421,21.5595C4.46861,21.5027,4.39038,21.4658,4.34951,21.4488C3.49789,21.0943,2.74367,20.5941,2.08684,19.9484C0.695613,18.5806,0,16.9311,0,15C0,13.0689,0.695612,11.4194,2.08684,10.0516C3.24259,8.91537,4.59607,8.2511,6.14728,8.05878C6.34758,5.97414,7.22633,4.16634,8.78354,2.63539C10.5706,0.878463,12.7286,0,15.2573,0C17.2884,0,19.1146,0.595472,20.7358,1.78642C22.327,2.95528,23.4151,4.46783,24,6.32406L22.0568,6.91594C21.6024,5.47377,20.7561,4.29798,19.5181,3.38858C18.2579,2.46286,16.8377,2,15.2573,2C13.2903,2,11.6119,2.6832,10.222,4.04961C8.83217,5.41601,8.13725,7.06614,8.13725,9L8.13725,10L7.12009,10C5.71758,10,4.51932,10.4886,3.52532,11.4659C2.53132,12.4431,2.03431,13.6211,2.03431,15C2.03431,16.3789,2.53132,17.5569,3.52532,18.5341C3.99531,18.9962,4.53447,19.3538,5.14278,19.6071C5.2229,19.6405,5.33983,19.695,5.49356,19.7705C5.80505,19.9235,6.00818,20,6.10294,20L6.10294,22Z",
"fill-rule": "evenodd",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
},
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M20.18796103515625,11.66909C19.46346103515625,11.5762,18.72726103515625,11.52975,17.991011035156248,11.52975C16.728921035156247,11.52975,15.45515103515625,11.66909,14.23981103515625,11.91292C13.02447103515625,12.156749999999999,11.85588103515625,12.539909999999999,10.73402103515625,12.98113C9.98612103515625,13.306239999999999,9.23822103515625,13.69327,8.49031803515625,14.14223C7.99950790415625,14.43251,7.85927603515625,15.10595,8.15142503515625,15.59361L11.11966103515625,19.9478C11.45855103515625,20.4354,12.13634103515625,20.5747,12.627151035156249,20.2845C12.821921035156251,20.152900000000002,13.14523103515625,19.990299999999998,13.59708103515625,19.796799999999998C14.27487103515625,19.506500000000003,14.964341035156249,19.3091,15.68887103515625,19.169800000000002C16.413401035156248,19.018900000000002,17.14962103515625,18.926000000000002,17.93258103515625,18.926000000000002L20.071061035156248,11.715530000000001L20.18796103515625,11.66909ZM22.91076103515625,12.20319L22.525161035156252,8L20.18796103515625,11.6807C20.72556103515625,11.72714,21.21636103515625,11.82003,21.74216103515625,11.92453C21.74216103515625,11.91679,22.13166103515625,12.00968,22.91076103515625,12.20319ZM18.09616103515625,18.9724L17.06782103515625,22.4557L18.773961035156248,24L21.11116103515625,23.465899999999998L21.788961035156248,19.5414C21.298161035156248,19.402,20.81896103515625,19.2511,20.32816103515625,19.1582C19.60366103515625,19.076900000000002,18.86746103515625,18.9724,18.09616103515625,18.9724ZM27.49166103515625,14.14223C26.74376103515625,13.69327,25.99586103515625,13.306239999999999,25.24796103515625,12.98113C24.52346103515625,12.69086,23.74046103515625,12.40058,22.95756103515625,12.20319L22.95756103515625,12.40058L21.69546103515625,19.5646C21.89416103515625,19.6575,22.139561035156248,19.7039,22.32646103515625,19.8084C22.77836103515625,20.0019,23.101661035156248,20.1645,23.29646103515625,20.2961C23.78726103515625,20.586399999999998,24.51176103515625,20.4354,24.80396103515625,19.959400000000002L27.77216103515625,15.605229999999999C28.16946103515625,15.05951,28.02926103515625,14.43251,27.49166103515625,14.14223Z",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
},
{
"type": "element",
"name": "g",
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M35.785,3.8624638671875L50.233000000000004,3.8624638671875L50.233000000000004,5.9204638671875L35.785,5.9204638671875L35.785,3.8624638671875ZM43.156,11.5904638671875Q42.106,13.4594638671875,40.7515,15.4754638671875Q39.397,17.4914638671875,38.599000000000004,18.3104638671875L46.978,17.5334638671875Q45.382,15.0974638671875,44.479,13.8794638671875L46.306,12.7034638671875Q47.397999999999996,14.1104638671875,48.9835,16.3784638671875Q50.569,18.6464638671875,51.492999999999995,20.1794638671875L49.54,21.6494638671875Q49.057,20.8094638671875,48.238,19.5074638671875Q46.243,19.6334638671875,42.82,19.9274638671875Q39.397,20.2214638671875,37.024,20.4734638671875L36.184,20.5784638671875L35.47,20.6834638671875L34.84,18.5624638671875Q35.281,18.4154638671875,35.512,18.2579638671875Q35.743,18.1004638671875,35.9005,17.963963867187502Q36.058,17.8274638671875,36.121,17.7644638671875Q37.087,16.840463867187502,38.305,15.1079638671875Q39.522999999999996,13.3754638671875,40.531,11.5904638671875L34,11.5904638671875L34,9.5114638671875L52.018,9.5114638671875L52.018,11.5904638671875L43.156,11.5904638671875ZM62.203,10.9814638671875L62.203,12.7244638671875L60.25,12.7244638671875L60.25,2.5814638671875L62.203,2.6654638671875L62.203,10.4144638671875Q63.19,8.6294638671875,64.051,6.4139638671875Q64.912,4.1984638671875,65.28999999999999,2.3504638671875L67.348,2.8334628671875Q67.15899999999999,3.7784638671875,66.80199999999999,4.9754638671875L72.619,4.9754638671875L72.619,6.9704638671875L66.13,6.9704638671875Q65.143,9.7004638671875,63.778,12.0524638671875L62.203,10.9814638671875ZM56.113,3.3794638671875L58.045,3.4634638671875L58.045,12.1784638671875L56.113,12.1784638671875L56.113,3.3794638671875ZM67.495,7.3064638671875Q68.251,7.8944638671875,69.469,9.1229638671875Q70.687,10.3514638671875,71.40100000000001,11.2334638671875L69.84700000000001,12.7454638671875Q69.238,11.9684638671875,68.083,10.7714638671875Q66.928,9.5744638671875,66.025,8.7134638671875L67.495,7.3064638671875ZM70.834,13.3754638671875L70.834,18.9194638671875L73.06,18.9194638671875L73.06,20.8094638671875L54.307,20.8094638671875L54.307,18.9194638671875L56.491,18.9194638671875L56.491,13.3754638671875L70.834,13.3754638671875ZM60.733000000000004,15.2444638671875L58.465,15.2444638671875L58.465,18.9194638671875L60.733000000000004,18.9194638671875L60.733000000000004,15.2444638671875ZM62.581,18.9194638671875L64.765,18.9194638671875L64.765,15.2444638671875L62.581,15.2444638671875L62.581,18.9194638671875ZM66.592,18.9194638671875L68.881,18.9194638671875L68.881,15.2444638671875L66.592,15.2444638671875L66.592,18.9194638671875ZM80.578,11.0444638671875L80.893,12.4514638671875L79.48599999999999,13.0814638671875L79.48599999999999,19.0874638671875Q79.48599999999999,20.0114638671875,79.2655,20.4629638671875Q79.045,20.9144638671875,78.52000000000001,21.1034638671875Q77.995,21.2924638671875,76.90299999999999,21.3974638671875L76.021,21.4814638671875L75.43299999999999,19.4864638671875L76.462,19.4024638671875Q76.987,19.3604638671875,77.197,19.2974638671875Q77.407,19.2344638671875,77.4805,19.0559638671875Q77.554,18.8774638671875,77.554,18.4364638671875L77.554,13.9004638671875Q76.189,14.4464638671875,75.202,14.7824638671875L74.74000000000001,12.7244638671875Q75.916,12.3464638671875,77.554,11.6744638671875L77.554,8.1464638671875L75.34899999999999,8.1464638671875L75.34899999999999,6.1094638671875L77.554,6.1094638671875L77.554,2.4974628671875L79.48599999999999,2.5814638671875L79.48599999999999,6.1094638671875L81.03999999999999,6.1094638671875L81.03999999999999,8.1464638671875L79.48599999999999,8.1464638671875L79.48599999999999,10.8344638671875L80.431,10.3934638671875L80.578,11.0444638671875ZM83.56,6.6764638671875L83.56,9.0074638671875L81.565,9.0074638671875L81.565,4.7444638671875L86.24799999999999,4.7444638671875Q85.84899999999999,3.3794638671875,85.618,2.7494638671875L87.655,2.4974628671875Q87.991,3.2744638671875,88.432,4.7444638671875L93.094,4.7444638671875L93.094,9.0074638671875L91.162,9.0074638671875L91.162,6.6764638671875L83.56,6.6764638671875ZM86.731,9.3434638671875Q85.807,10.2674638671875,84.7465,11.1284638671875Q83.686,11.9894638671875,82.15299999999999,13.1234638671875L81.082,11.5064638671875Q83.455,9.9524638671875,85.408,7.9154638671875L86.731,9.3434638671875ZM88.852,7.9154638671875Q89.755,8.5244638671875,91.3615,9.731963867187499Q92.968,10.9394638671875,93.703,11.5694638671875L92.632,13.3334638671875Q91.771,12.5354638671875,90.217,11.3384638671875Q88.663,10.1414638671875,87.718,9.5114638671875L88.852,7.9154638671875ZM92.107,15.2444638671875L88.285,15.2444638671875L88.285,18.7094638671875L93.577,18.7094638671875L93.577,20.5994638671875L80.935,20.5994638671875L80.935,18.7094638671875L86.164,18.7094638671875L86.164,15.2444638671875L82.3,15.2444638671875L82.3,13.3334638671875L92.107,13.3334638671875L92.107,15.2444638671875Z",
"fill": "#FF6A00",
"fill-opacity": "1"
}
}
]
}
]
}
]
},
"name": "AliyunBigIcon"
}

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AliyunIconBig.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'AliyunIconBig'
export default Icon

@ -11,3 +11,5 @@ export { default as PhoenixIcon } from './PhoenixIcon'
export { default as TracingIcon } from './TracingIcon' export { default as TracingIcon } from './TracingIcon'
export { default as WeaveIconBig } from './WeaveIconBig' export { default as WeaveIconBig } from './WeaveIconBig'
export { default as WeaveIcon } from './WeaveIcon' export { default as WeaveIcon } from './WeaveIcon'
export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'

@ -28,6 +28,7 @@ enum SUPPORTED_TYPES {
DATETIME = 'datetime', DATETIME = 'datetime',
CHECKBOX = 'checkbox', CHECKBOX = 'checkbox',
SELECT = 'select', SELECT = 'select',
HIDDEN = 'hidden',
} }
const MarkdownForm = ({ node }: any) => { const MarkdownForm = ({ node }: any) => {
const { onSend } = useChatContext() const { onSend } = useChatContext()
@ -37,8 +38,12 @@ const MarkdownForm = ({ node }: any) => {
useEffect(() => { useEffect(() => {
const initialValues: { [key: string]: any } = {} const initialValues: { [key: string]: any } = {}
node.children.forEach((child: any) => { node.children.forEach((child: any) => {
if ([SUPPORTED_TAGS.INPUT, SUPPORTED_TAGS.TEXTAREA].includes(child.tagName)) if ([SUPPORTED_TAGS.INPUT, SUPPORTED_TAGS.TEXTAREA].includes(child.tagName)) {
initialValues[child.properties.name] = child.properties.value initialValues[child.properties.name]
= (child.tagName === SUPPORTED_TAGS.INPUT && child.properties.type === SUPPORTED_TYPES.HIDDEN)
? (child.properties.value || '')
: child.properties.value
}
}) })
setFormValues(initialValues) setFormValues(initialValues)
}, [node.children]) }, [node.children])
@ -180,6 +185,17 @@ const MarkdownForm = ({ node }: any) => {
) )
} }
if (child.properties.type === SUPPORTED_TYPES.HIDDEN) {
return (
<input
key={index}
type="hidden"
name={child.properties.name}
value={formValues[child.properties.name] || child.properties.value || ''}
/>
)
}
return ( return (
<Input <Input
key={index} key={index}

@ -116,7 +116,7 @@ const Select: FC<ISelectProps> = ({
if (!disabled) if (!disabled)
setOpen(!open) setOpen(!open)
} }
} className={classNames(`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover group-hover:bg-state-base-hover`, optionClassName)}> } className={classNames(`flex h-9 w-full items-center rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6`, optionClassName)}>
<div className='w-0 grow truncate text-left' title={selectedItem?.name}>{selectedItem?.name}</div> <div className='w-0 grow truncate text-left' title={selectedItem?.name}>{selectedItem?.name}</div>
</ComboboxButton>} </ComboboxButton>}
<ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" onClick={ <ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" onClick={
@ -137,7 +137,7 @@ const Select: FC<ISelectProps> = ({
value={item} value={item}
className={({ active }: { active: boolean }) => className={({ active }: { active: boolean }) =>
classNames( classNames(
'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', 'relative cursor-default select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
active ? 'bg-state-base-hover' : '', active ? 'bg-state-base-hover' : '',
optionClassName, optionClassName,
) )
@ -225,8 +225,8 @@ const SimpleSelect: FC<ISelectProps> = ({
if (listboxRef.current) if (listboxRef.current)
onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null) onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null)
}) })
}} className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> }} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> <span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2"> <span className="absolute inset-y-0 right-0 flex items-center pr-2">
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' /> {isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
: (selectedItem && !notClearable) : (selectedItem && !notClearable)
@ -252,13 +252,13 @@ const SimpleSelect: FC<ISelectProps> = ({
)} )}
{(!disabled) && ( {(!disabled) && (
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> <ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => ( {items.map((item: Item) => (
<ListboxOption <ListboxOption
key={item.value} key={item.value}
className={ className={
classNames( classNames(
'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', 'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
optionClassName, optionClassName,
) )
} }
@ -338,7 +338,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
: ( : (
<div <div
className={classNames(` className={classNames(`
group flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-components-input-bg-normal hover:bg-state-base-hover-alt text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'} group flex h-9 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2.5 text-sm hover:bg-state-base-hover-alt ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}
`, triggerClassName, triggerClassNameFn?.(open))} `, triggerClassName, triggerClassNameFn?.(open))}
title={selectedItem?.name} title={selectedItem?.name}
> >
@ -358,7 +358,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={`z-20 ${popupClassName}`}> <PortalToFollowElemContent className={`z-20 ${popupClassName}`}>
<div <div
className={classNames('px-1 py-1 max-h-60 overflow-auto rounded-md text-base shadow-lg border-components-panel-border bg-components-panel-bg border-[0.5px] focus:outline-none sm:text-sm', popupInnerClassName)} className={classNames('max-h-60 overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)}
> >
{items.map((item: Item) => ( {items.map((item: Item) => (
<div <div

@ -0,0 +1,83 @@
import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useMemo } from 'react'
export type DocumentListQuery = {
page: number
limit: number
keyword: string
}
const DEFAULT_QUERY: DocumentListQuery = {
page: 1,
limit: 10,
keyword: '',
}
// Parse the query parameters from the URL search string.
function parseParams(params: ReadonlyURLSearchParams): DocumentListQuery {
const page = Number.parseInt(params.get('page') || '1', 10)
const limit = Number.parseInt(params.get('limit') || '10', 10)
const keyword = params.get('keyword') || ''
return {
page: page > 0 ? page : 1,
limit: (limit > 0 && limit <= 100) ? limit : 10,
keyword: keyword ? decodeURIComponent(keyword) : '',
}
}
// Update the URL search string with the given query parameters.
function updateSearchParams(query: DocumentListQuery, searchParams: URLSearchParams) {
const { page, limit, keyword } = query || {}
const hasNonDefaultParams = (page && page > 1) || (limit && limit !== 10) || (keyword && keyword.trim())
if (hasNonDefaultParams) {
searchParams.set('page', (page || 1).toString())
searchParams.set('limit', (limit || 10).toString())
}
else {
searchParams.delete('page')
searchParams.delete('limit')
}
if (keyword && keyword.trim())
searchParams.set('keyword', encodeURIComponent(keyword))
else
searchParams.delete('keyword')
}
function useDocumentListQueryState() {
const searchParams = useSearchParams()
const query = useMemo(() => parseParams(searchParams), [searchParams])
const router = useRouter()
const pathname = usePathname()
// Helper function to update specific query parameters
const updateQuery = useCallback((updates: Partial<DocumentListQuery>) => {
const newQuery = { ...query, ...updates }
const params = new URLSearchParams()
updateSearchParams(newQuery, params)
const search = params.toString()
const queryString = search ? `?${search}` : ''
router.push(`${pathname}${queryString}`, { scroll: false })
}, [query, router, pathname])
// Helper function to reset query to defaults
const resetQuery = useCallback(() => {
const params = new URLSearchParams()
updateSearchParams(DEFAULT_QUERY, params)
const search = params.toString()
const queryString = search ? `?${search}` : ''
router.push(`${pathname}${queryString}`, { scroll: false })
}, [router, pathname])
return useMemo(() => ({
query,
updateQuery,
resetQuery,
}), [query, updateQuery, resetQuery])
}
export default useDocumentListQueryState

@ -26,6 +26,7 @@ import cn from '@/utils/classnames'
import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document' import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document'
import { useInvalid } from '@/service/use-base' import { useInvalid } from '@/service/use-base'
import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment' import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment'
import useDocumentListQueryState from './hooks/use-document-list-query-state'
import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata' import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata'
import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer' import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer'
import StatusWithAction from '../common/document-status-with-action/status-with-action' import StatusWithAction from '../common/document-status-with-action/status-with-action'
@ -82,7 +83,6 @@ type IDocumentsProps = {
} }
export const fetcher = (url: string) => get(url, {}, {}) export const fetcher = (url: string) => get(url, {}, {})
const DEFAULT_LIMIT = 10
const Documents: FC<IDocumentsProps> = ({ datasetId }) => { const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -91,8 +91,12 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const isFreePlan = plan.type === 'sandbox' const isFreePlan = plan.type === 'sandbox'
const [inputValue, setInputValue] = useState<string>('') // the input value const [inputValue, setInputValue] = useState<string>('') // the input value
const [searchValue, setSearchValue] = useState<string>('') const [searchValue, setSearchValue] = useState<string>('')
const [currPage, setCurrPage] = React.useState<number>(0)
const [limit, setLimit] = useState<number>(DEFAULT_LIMIT) // Use the new hook for URL state management
const { query, updateQuery } = useDocumentListQueryState()
const [currPage, setCurrPage] = React.useState<number>(query.page - 1) // Convert to 0-based index
const [limit, setLimit] = useState<number>(query.limit)
const router = useRouter() const router = useRouter()
const { dataset } = useDatasetDetailContext() const { dataset } = useDatasetDetailContext()
const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false) const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false)
@ -103,6 +107,45 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
const embeddingAvailable = !!dataset?.embedding_available const embeddingAvailable = !!dataset?.embedding_available
const debouncedSearchValue = useDebounce(searchValue, { wait: 500 }) const debouncedSearchValue = useDebounce(searchValue, { wait: 500 })
// Initialize search value from URL on mount
useEffect(() => {
if (query.keyword) {
setInputValue(query.keyword)
setSearchValue(query.keyword)
}
}, []) // Only run on mount
// Sync local state with URL query changes
useEffect(() => {
setCurrPage(query.page - 1)
setLimit(query.limit)
if (query.keyword !== searchValue) {
setInputValue(query.keyword)
setSearchValue(query.keyword)
}
}, [query])
// Update URL when pagination changes
const handlePageChange = (newPage: number) => {
setCurrPage(newPage)
updateQuery({ page: newPage + 1 }) // Convert to 1-based index
}
// Update URL when limit changes
const handleLimitChange = (newLimit: number) => {
setLimit(newLimit)
setCurrPage(0) // Reset to first page when limit changes
updateQuery({ limit: newLimit, page: 1 })
}
// Update URL when search changes
useEffect(() => {
if (debouncedSearchValue !== query.keyword) {
setCurrPage(0) // Reset to first page when search changes
updateQuery({ keyword: debouncedSearchValue, page: 1 })
}
}, [debouncedSearchValue, query.keyword, updateQuery])
const { data: documentsRes, isFetching: isListLoading } = useDocumentList({ const { data: documentsRes, isFetching: isListLoading } = useDocumentList({
datasetId, datasetId,
query: { query: {
@ -327,9 +370,9 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
pagination={{ pagination={{
total, total,
limit, limit,
onLimitChange: setLimit, onLimitChange: handleLimitChange,
current: currPage, current: currPage,
onChange: setCurrPage, onChange: handlePageChange,
}} }}
onManageMetadata={showEditMetadataModal} onManageMetadata={showEditMetadataModal}
/> />

@ -11,6 +11,8 @@ import {
RiEqualizer2Line, RiEqualizer2Line,
RiLoopLeftLine, RiLoopLeftLine,
RiMoreFill, RiMoreFill,
RiPauseCircleLine,
RiPlayCircleLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
@ -42,7 +44,7 @@ import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '
import type { Props as PaginationProps } from '@/app/components/base/pagination' import type { Props as PaginationProps } from '@/app/components/base/pagination'
import Pagination from '@/app/components/base/pagination' import Pagination from '@/app/components/base/pagination'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document' import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentPause, useDocumentResume, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document'
import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata' import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal' import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
@ -168,7 +170,7 @@ export const StatusItem: FC<{
</div> </div>
} }
type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' | 'pause' | 'resume'
// operation action for list and detail // operation action for list and detail
export const OperationAction: FC<{ export const OperationAction: FC<{
@ -180,13 +182,14 @@ export const OperationAction: FC<{
id: string id: string
data_source_type: string data_source_type: string
doc_form: string doc_form: string
display_status?: string
} }
datasetId: string datasetId: string
onUpdate: (operationName?: string) => void onUpdate: (operationName?: string) => void
scene?: 'list' | 'detail' scene?: 'list' | 'detail'
className?: string className?: string
}> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => { }> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => {
const { id, enabled = false, archived = false, data_source_type } = detail || {} const { id, enabled = false, archived = false, data_source_type, display_status } = detail || {}
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
@ -199,6 +202,8 @@ export const OperationAction: FC<{
const { mutateAsync: deleteDocument } = useDocumentDelete() const { mutateAsync: deleteDocument } = useDocumentDelete()
const { mutateAsync: syncDocument } = useSyncDocument() const { mutateAsync: syncDocument } = useSyncDocument()
const { mutateAsync: syncWebsite } = useSyncWebsite() const { mutateAsync: syncWebsite } = useSyncWebsite()
const { mutateAsync: pauseDocument } = useDocumentPause()
const { mutateAsync: resumeDocument } = useDocumentResume()
const isListScene = scene === 'list' const isListScene = scene === 'list'
const onOperate = async (operationName: OperationName) => { const onOperate = async (operationName: OperationName) => {
@ -222,6 +227,12 @@ export const OperationAction: FC<{
else else
opApi = syncWebsite opApi = syncWebsite
break break
case 'pause':
opApi = pauseDocument
break
case 'resume':
opApi = resumeDocument
break
default: default:
opApi = deleteDocument opApi = deleteDocument
setDeleting(true) setDeleting(true)
@ -323,6 +334,18 @@ export const OperationAction: FC<{
<Divider className='my-1' /> <Divider className='my-1' />
</> </>
)} )}
{!archived && display_status?.toLowerCase() === 'indexing' && (
<div className={s.actionItem} onClick={() => onOperate('pause')}>
<RiPauseCircleLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.pause')}</span>
</div>
)}
{!archived && display_status?.toLowerCase() === 'paused' && (
<div className={s.actionItem} onClick={() => onOperate('resume')}>
<RiPlayCircleLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.resume')}</span>
</div>
)}
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}> {!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
<RiArchive2Line className='h-4 w-4 text-text-tertiary' /> <RiArchive2Line className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span> <span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
@ -575,7 +598,6 @@ const DocumentList: FC<IDocumentListProps> = ({
) )
}} }}
/> />
{/* {doc.position} */}
{index + 1} {index + 1}
</div> </div>
</td> </td>
@ -626,7 +648,7 @@ const DocumentList: FC<IDocumentListProps> = ({
<OperationAction <OperationAction
embeddingAvailable={embeddingAvailable} embeddingAvailable={embeddingAvailable}
datasetId={datasetId} datasetId={datasetId}
detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form'])} detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'display_status'])}
onUpdate={onUpdate} onUpdate={onUpdate}
/> />
</td> </td>

@ -36,7 +36,7 @@ import { useDocLink } from '@/context/i18n'
export default function AppSelector() { export default function AppSelector() {
const itemClassName = ` const itemClassName = `
flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular flex items-center w-full h-8 pl-3 pr-2 text-text-secondary system-md-regular
rounded-lg hover:bg-state-base-hover cursor-pointer gap-1 rounded-lg hover:bg-state-base-hover cursor-pointer gap-1
` `
const router = useRouter() const router = useRouter()
@ -87,8 +87,9 @@ export default function AppSelector() {
backdrop-blur-sm focus:outline-none backdrop-blur-sm focus:outline-none
" "
> >
<div className="px-1 py-1">
<MenuItem disabled> <MenuItem disabled>
<div className='flex flex-nowrap items-center py-[13px] pl-3 pr-2'> <div className='flex flex-nowrap items-center py-2 pl-3 pr-2'>
<div className='grow'> <div className='grow'>
<div className='system-md-medium break-all text-text-primary'> <div className='system-md-medium break-all text-text-primary'>
{userProfile.name} {userProfile.name}
@ -101,10 +102,9 @@ export default function AppSelector() {
</div> </div>
<div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div>
</div> </div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' /> <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
</div> </div>
</MenuItem> </MenuItem>
<div className="px-1 py-1">
<MenuItem> <MenuItem>
<Link <Link
className={cn(itemClassName, 'group', className={cn(itemClassName, 'group',

@ -54,7 +54,7 @@ const Nav = ({
<div <div
onClick={() => setAppDetail()} onClick={() => setAppDetail()}
className={classNames(` className={classNames(`
flex items-center h-7 px-2.5 cursor-pointer rounded-[10px] flex h-7 cursor-pointer items-center rounded-[10px] px-2.5
${isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text'} ${isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text'}
${curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover'} ${curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover'}
`)} `)}

@ -31,8 +31,8 @@ const PluginsNav = ({
)}> )}>
<div <div
className={classNames( className={classNames(
'relative flex flex-row h-8 p-1.5 gap-0.5 border border-transparent items-center justify-center rounded-xl system-sm-medium', 'system-sm-medium relative flex h-8 flex-row items-center justify-center gap-0.5 rounded-xl border border-transparent p-1.5',
activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active shadow-md text-components-main-nav-nav-button-text', activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text shadow-md',
!activated && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', !activated && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
(isInstallingWithError || isFailed) && !activated && 'border-components-panel-border-subtle', (isInstallingWithError || isFailed) && !activated && 'border-components-panel-border-subtle',
)} )}

@ -22,7 +22,7 @@ const ToolsNav = ({
return ( return (
<Link href="/tools" className={classNames( <Link href="/tools" className={classNames(
'group text-sm font-medium', 'group text-sm font-medium',
activated && 'font-semibold bg-components-main-nav-nav-button-bg-active hover:bg-components-main-nav-nav-button-bg-active-hover shadow-md', activated && 'hover:bg-components-main-nav-nav-button-bg-active-hover bg-components-main-nav-nav-button-bg-active font-semibold shadow-md',
activated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover', activated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover',
className, className,
)}> )}>

@ -98,7 +98,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
}, []) }, [])
return ( return (
<div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', hideTopMenu && 'pt-2', className)}> <div className={classNames('flex h-full flex-col overflow-hidden bg-components-input-bg-normal', hideTopMenu && 'pt-2', className)}>
{!hideTopMenu && ( {!hideTopMenu && (
<div className='flex items-center justify-between pl-2 pr-1 pt-1'> <div className='flex items-center justify-between pl-2 pr-1 pt-1'>
<div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'> <div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'>

@ -10,6 +10,8 @@ import type {
LoopVariable, LoopVariable,
LoopVariablesComponentShape, LoopVariablesComponentShape,
} from '@/app/components/workflow/nodes/loop/types' } from '@/app/components/workflow/nodes/loop/types'
import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var'
import Toast from '@/app/components/base/toast'
type ItemProps = { type ItemProps = {
item: LoopVariable item: LoopVariable
@ -21,7 +23,22 @@ const Item = ({
handleUpdateLoopVariable, handleUpdateLoopVariable,
}: ItemProps) => { }: ItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const checkVariableName = (value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }),
})
return false
}
return true
}
const handleUpdateItemLabel = useCallback((e: any) => { const handleUpdateItemLabel = useCallback((e: any) => {
replaceSpaceWithUnderscreInVarNameInput(e.target)
if (!!e.target.value && !checkVariableName(e.target.value))
return
handleUpdateLoopVariable(item.id, { label: e.target.value }) handleUpdateLoopVariable(item.id, { label: e.target.value })
}, [item.id, handleUpdateLoopVariable]) }, [item.id, handleUpdateLoopVariable])
@ -44,6 +61,7 @@ const Item = ({
<Input <Input
value={item.label} value={item.label}
onChange={handleUpdateItemLabel} onChange={handleUpdateItemLabel}
onBlur={e => checkVariableName(e.target.value)}
autoFocus={!item.label} autoFocus={!item.label}
placeholder={t('workflow.nodes.loop.variableName')} placeholder={t('workflow.nodes.loop.variableName')}
/> />

@ -32,7 +32,7 @@ export default function SocialAuth(props: SocialAuthProps) {
<span className={ <span className={
classNames( classNames(
style.githubIcon, style.githubIcon,
'w-5 h-5 mr-2', 'mr-2 h-5 w-5',
) )
} /> } />
<span className="truncate">{t('login.withGitHub')}</span> <span className="truncate">{t('login.withGitHub')}</span>
@ -50,7 +50,7 @@ export default function SocialAuth(props: SocialAuthProps) {
<span className={ <span className={
classNames( classNames(
style.googleIcon, style.googleIcon,
'w-5 h-5 mr-2', 'mr-2 h-5 w-5',
) )
} /> } />
<span className="truncate">{t('login.withGoogle')}</span> <span className="truncate">{t('login.withGoogle')}</span>

@ -28,6 +28,8 @@ const translation = {
delete: 'Löschen', delete: 'Löschen',
enableWarning: 'Archivierte Datei kann nicht aktiviert werden', enableWarning: 'Archivierte Datei kann nicht aktiviert werden',
sync: 'Synchronisieren', sync: 'Synchronisieren',
resume: 'Fortsetzen',
pause: 'Pause',
}, },
index: { index: {
enable: 'Aktivieren', enable: 'Aktivieren',

@ -174,6 +174,10 @@ const translation = {
title: 'Weave', title: 'Weave',
description: 'Weave is an open-source platform for evaluating, testing, and monitoring LLM applications.', description: 'Weave is an open-source platform for evaluating, testing, and monitoring LLM applications.',
}, },
aliyun: {
title: 'LLM observability',
description: 'The SaaS observability platform provided by Alibaba Cloud enables out of box monitoring, tracing, and evaluation of Dify applications.',
},
inUse: 'In use', inUse: 'In use',
configProvider: { configProvider: {
title: 'Config ', title: 'Config ',

@ -30,6 +30,8 @@ const translation = {
delete: 'Delete', delete: 'Delete',
enableWarning: 'Archived file cannot be enabled', enableWarning: 'Archived file cannot be enabled',
sync: 'Sync', sync: 'Sync',
pause: 'Pause',
resume: 'Resume',
}, },
index: { index: {
enable: 'Enable', enable: 'Enable',

@ -29,6 +29,8 @@ const translation = {
delete: 'Eliminar', delete: 'Eliminar',
enableWarning: 'El archivo archivado no puede habilitarse', enableWarning: 'El archivo archivado no puede habilitarse',
sync: 'Sincronizar', sync: 'Sincronizar',
resume: 'Reanudar',
pause: 'Pausa',
}, },
index: { index: {
enable: 'Habilitar', enable: 'Habilitar',

@ -29,6 +29,8 @@ const translation = {
delete: 'حذف', delete: 'حذف',
enableWarning: 'فایل بایگانی شده نمی‌تواند فعال شود', enableWarning: 'فایل بایگانی شده نمی‌تواند فعال شود',
sync: 'همگام‌سازی', sync: 'همگام‌سازی',
resume: 'ادامه',
pause: 'مکث',
}, },
index: { index: {
enable: 'فعال کردن', enable: 'فعال کردن',

@ -28,6 +28,8 @@ const translation = {
delete: 'Supprimer', delete: 'Supprimer',
enableWarning: 'Le fichier archivé ne peut pas être activé', enableWarning: 'Le fichier archivé ne peut pas être activé',
sync: 'Synchroniser', sync: 'Synchroniser',
pause: 'Pause',
resume: 'Reprendre',
}, },
index: { index: {
enable: 'Activer', enable: 'Activer',

@ -29,6 +29,8 @@ const translation = {
delete: 'हटाएँ', delete: 'हटाएँ',
enableWarning: 'संग्रहित फाइल को सक्रिय नहीं किया जा सकता', enableWarning: 'संग्रहित फाइल को सक्रिय नहीं किया जा सकता',
sync: 'सिंक्रोनाइज़ करें', sync: 'सिंक्रोनाइज़ करें',
resume: 'रिज़्यूमे',
pause: 'रोकें',
}, },
index: { index: {
enable: 'सक्रिय करें', enable: 'सक्रिय करें',

@ -29,6 +29,8 @@ const translation = {
delete: 'Elimina', delete: 'Elimina',
enableWarning: 'Il file archiviato non può essere abilitato', enableWarning: 'Il file archiviato non può essere abilitato',
sync: 'Sincronizza', sync: 'Sincronizza',
resume: 'Riassumere',
pause: 'Pausa',
}, },
index: { index: {
enable: 'Abilita', enable: 'Abilita',

@ -30,6 +30,8 @@ const translation = {
delete: '削除', delete: '削除',
enableWarning: 'アーカイブされたファイルは有効にできません', enableWarning: 'アーカイブされたファイルは有効にできません',
sync: '同期', sync: '同期',
pause: '一時停止',
resume: '再開',
}, },
index: { index: {
enable: '有効にする', enable: '有効にする',

@ -16,7 +16,8 @@ const translation = {
importDSL: 'DSL 파일 가져오기', importDSL: 'DSL 파일 가져오기',
createFromConfigFile: 'DSL 파일에서 생성하기', createFromConfigFile: 'DSL 파일에서 생성하기',
deleteAppConfirmTitle: '이 앱을 삭제하시겠습니까?', deleteAppConfirmTitle: '이 앱을 삭제하시겠습니까?',
deleteAppConfirmContent: '앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.', deleteAppConfirmContent:
'앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.',
appDeleted: '앱이 삭제되었습니다', appDeleted: '앱이 삭제되었습니다',
appDeleteFailed: '앱 삭제 실패', appDeleteFailed: '앱 삭제 실패',
join: '커뮤니티에 참여하기', join: '커뮤니티에 참여하기',
@ -26,20 +27,25 @@ const translation = {
startFromBlank: '빈 상태로 시작', startFromBlank: '빈 상태로 시작',
startFromTemplate: '템플릿에서 시작', startFromTemplate: '템플릿에서 시작',
captionAppType: '어떤 종류의 앱을 만들어 보시겠어요?', captionAppType: '어떤 종류의 앱을 만들어 보시겠어요?',
chatbotDescription: '대화형 어플리케이션을 만듭니다. 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', chatbotDescription:
completionDescription: '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만듭니다. 기사, 요약, 번역 등을 생성할 수 있습니다.', '대화형 어플리케이션을 만듭니다. 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.',
completionDescription:
'프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만듭니다. 기사, 요약, 번역 등을 생성할 수 있습니다.',
completionWarning: '이 종류의 앱은 더 이상 지원되지 않습니다.', completionWarning: '이 종류의 앱은 더 이상 지원되지 않습니다.',
agentDescription: '작업을 자동으로 완료하는 지능형 에이전트를 만듭니다.', agentDescription: '작업을 자동으로 완료하는 지능형 에이전트를 만듭니다.',
workflowDescription: '고도로 사용자 지정 가능한 워크플로우에 기반한 고품질 텍스트 생성 어플리케이션을 만듭니다. 경험 있는 사용자를 위한 것입니다.', workflowDescription:
'고도로 사용자 지정 가능한 워크플로우에 기반한 고품질 텍스트 생성 어플리케이션을 만듭니다. 경험 있는 사용자를 위한 것입니다.',
workflowWarning: '현재 베타 버전입니다.', workflowWarning: '현재 베타 버전입니다.',
chatbotType: '챗봇 오케스트레이션 방식', chatbotType: '챗봇 오케스트레이션 방식',
basic: '기본', basic: '기본',
basicTip: '초보자용. 나중에 Chatflow 로 전환할 수 있습니다.', basicTip: '초보자용. 나중에 Chatflow 로 전환할 수 있습니다.',
basicFor: '초보자용', basicFor: '초보자용',
basicDescription: '기본 오케스트레이션은 내장된 프롬프트를 수정할 수 없고 간단한 설정을 사용하여 챗봇 앱을 오케스트레이션합니다. 초보자용입니다.', basicDescription:
'기본 오케스트레이션은 내장된 프롬프트를 수정할 수 없고 간단한 설정을 사용하여 챗봇 앱을 오케스트레이션합니다. 초보자용입니다.',
advanced: 'Chatflow', advanced: 'Chatflow',
advancedFor: '고급 사용자용', advancedFor: '고급 사용자용',
advancedDescription: '워크플로우 오케스트레이션은 워크플로우 형식으로 챗봇을 오케스트레이션하며 내장된 프롬프트를 편집할 수 있는 고급 사용자 정의 기능을 제공합니다. 경험이 많은 사용자용입니다.', advancedDescription:
'워크플로우 오케스트레이션은 워크플로우 형식으로 챗봇을 오케스트레이션하며 내장된 프롬프트를 편집할 수 있는 고급 사용자 정의 기능을 제공합니다. 경험이 많은 사용자용입니다.',
captionName: '앱 아이콘과 이름', captionName: '앱 아이콘과 이름',
appNamePlaceholder: '앱 이름을 입력하세요', appNamePlaceholder: '앱 이름을 입력하세요',
captionDescription: '설명', captionDescription: '설명',
@ -47,10 +53,12 @@ const translation = {
useTemplate: '이 템플릿 사용', useTemplate: '이 템플릿 사용',
previewDemo: '데모 미리보기', previewDemo: '데모 미리보기',
chatApp: '어시스턴트', chatApp: '어시스턴트',
chatAppIntro: '대화형 어플리케이션을 만들고 싶어요. 이 어플리케이션은 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', chatAppIntro:
'대화형 어플리케이션을 만들고 싶어요. 이 어플리케이션은 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.',
agentAssistant: '새로운 에이전트 어시스턴트', agentAssistant: '새로운 에이전트 어시스턴트',
completeApp: '텍스트 생성기', completeApp: '텍스트 생성기',
completeAppIntro: '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만들고 싶어요. 기사, 요약, 번역 등을 생성합니다.', completeAppIntro:
'프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만들고 싶어요. 기사, 요약, 번역 등을 생성합니다.',
showTemplates: '템플릿 선택', showTemplates: '템플릿 선택',
hideTemplates: '모드 선택으로 돌아가기', hideTemplates: '모드 선택으로 돌아가기',
Create: '만들기', Create: '만들기',
@ -66,13 +74,16 @@ const translation = {
appCreateDSLErrorTitle: '버전 비호환성', appCreateDSLErrorTitle: '버전 비호환성',
appCreateDSLErrorPart2: '계속하시겠습니까?', appCreateDSLErrorPart2: '계속하시겠습니까?',
appCreateDSLErrorPart3: '현재 응용 프로그램 DSL 버전:', appCreateDSLErrorPart3: '현재 응용 프로그램 DSL 버전:',
appCreateDSLWarning: '주의: DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', appCreateDSLWarning:
appCreateDSLErrorPart1: 'DSL 버전에서 상당한 차이가 감지되었습니다. 강제로 가져오면 응용 프로그램이 오작동할 수 있습니다.', '주의: DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.',
appCreateDSLErrorPart1:
'DSL 버전에서 상당한 차이가 감지되었습니다. 강제로 가져오면 응용 프로그램이 오작동할 수 있습니다.',
chooseAppType: '앱 유형 선택', chooseAppType: '앱 유형 선택',
forBeginners: '초보자용 기본 앱 유형', forBeginners: '초보자용 기본 앱 유형',
forAdvanced: '고급 사용자용', forAdvanced: '고급 사용자용',
chatbotShortDescription: '간단한 설정으로 LLM 기반 챗봇', chatbotShortDescription: '간단한 설정으로 LLM 기반 챗봇',
workflowUserDescription: '드래그 앤 드롭으로 자율 AI 워크플로우를 시각적으로 구축', workflowUserDescription:
'드래그 앤 드롭으로 자율 AI 워크플로우를 시각적으로 구축',
noTemplateFoundTip: '다른 키워드를 사용하여 검색해 보십시오.', noTemplateFoundTip: '다른 키워드를 사용하여 검색해 보십시오.',
noIdeaTip: '아이디어가 없으신가요? 템플릿을 확인해 보세요', noIdeaTip: '아이디어가 없으신가요? 템플릿을 확인해 보세요',
optional: '선택적', optional: '선택적',
@ -80,16 +91,20 @@ const translation = {
completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미', completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미',
learnMore: '더 알아보세요', learnMore: '더 알아보세요',
foundResults: '{{개수}} 결과', foundResults: '{{개수}} 결과',
agentShortDescription: '추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트', agentShortDescription:
'추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트',
advancedShortDescription: '다중 대화를 위해 강화된 워크플로우', advancedShortDescription: '다중 대화를 위해 강화된 워크플로우',
noAppsFound: '앱을 찾을 수 없습니다.', noAppsFound: '앱을 찾을 수 없습니다.',
foundResult: '{{개수}} 결과', foundResult: '{{개수}} 결과',
completionUserDescription: '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', completionUserDescription:
chatbotUserDescription: '간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow 로 전환할 수 있습니다.', '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.',
chatbotUserDescription:
'간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow 로 전환할 수 있습니다.',
workflowShortDescription: '지능형 자동화를 위한 에이전트 플로우', workflowShortDescription: '지능형 자동화를 위한 에이전트 플로우',
agentUserDescription: '작업 목표를 달성하기 위해 반복적인 추론과 자율적인 도구를 사용할 수 있는 지능형 에이전트입니다.', agentUserDescription:
'작업 목표를 달성하기 위해 반복적인 추론과 자율적인 도구를 사용할 수 있는 지능형 에이전트입니다.',
advancedUserDescription: '메모리 기능과 챗봇 인터페이스를 갖춘 워크플로우', advancedUserDescription: '메모리 기능과 챗봇 인터페이스를 갖춘 워크플로우',
dropDSLToCreateApp: '여기에 DSL 파일을 드롭하여 앱을 만드세요.', dropDSLToCreateApp: '여기에 DSL 파일을 드롭하여 앱을 불러오세요.',
}, },
editApp: '정보 편집하기', editApp: '정보 편집하기',
editAppTitle: '앱 정보 편집하기', editAppTitle: '앱 정보 편집하기',
@ -102,7 +117,8 @@ const translation = {
image: '이미지', image: '이미지',
}, },
switch: '워크플로우 오케스트레이션으로 전환하기', switch: '워크플로우 오케스트레이션으로 전환하기',
switchTipStart: '새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ', switchTipStart:
'새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ',
switchTip: '전환을 허용하지 않습니다', switchTip: '전환을 허용하지 않습니다',
switchTipEnd: ' 기본적인 오케스트레이션으로 되돌릴 수 없습니다.', switchTipEnd: ' 기본적인 오케스트레이션으로 되돌릴 수 없습니다.',
switchLabel: '생성될 앱의 복사본', switchLabel: '생성될 앱의 복사본',
@ -126,7 +142,8 @@ const translation = {
disabled: '비활성화됨', disabled: '비활성화됨',
disabledTip: '먼저 제공업체를 구성해 주세요', disabledTip: '먼저 제공업체를 구성해 주세요',
enabled: '서비스 중', enabled: '서비스 중',
tracingDescription: 'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제 3 자 추적 플랫폼에 캡처합니다.', tracingDescription:
'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제 3 자 추적 플랫폼에 캡처합니다.',
configProviderTitle: { configProviderTitle: {
configured: '구성됨', configured: '구성됨',
notConfigured: '추적을 활성화하려면 제공업체를 구성하세요', notConfigured: '추적을 활성화하려면 제공업체를 구성하세요',
@ -134,19 +151,23 @@ const translation = {
}, },
arize: { arize: {
title: 'Arize', title: 'Arize',
description: '엔터프라이즈급 LLM 가시성, 온라인 및 오프라인 평가, 모니터링 및 실험—OpenTelemetry를 기반으로 합니다. LLM 및 에이전트 기반 애플리케이션을 위해 특별히 설계되었습니다.', description:
'엔터프라이즈급 LLM 가시성, 온라인 및 오프라인 평가, 모니터링 및 실험—OpenTelemetry를 기반으로 합니다. LLM 및 에이전트 기반 애플리케이션을 위해 특별히 설계되었습니다.',
}, },
phoenix: { phoenix: {
title: 'Phoenix', title: 'Phoenix',
description: '오픈소스 및 OpenTelemetry 기반의 가시성, 평가, 프롬프트 엔지니어링 및 실험 플랫폼으로, LLM 워크플로우 및 에이전트를 지원합니다.', description:
'오픈소스 및 OpenTelemetry 기반의 가시성, 평가, 프롬프트 엔지니어링 및 실험 플랫폼으로, LLM 워크플로우 및 에이전트를 지원합니다.',
}, },
langsmith: { langsmith: {
title: 'LangSmith', title: 'LangSmith',
description: 'LLM 기반 애플리케이션 수명 주기의 모든 단계를 위한 올인원 개발자 플랫폼.', description:
'LLM 기반 애플리케이션 수명 주기의 모든 단계를 위한 올인원 개발자 플랫폼.',
}, },
langfuse: { langfuse: {
title: 'Langfuse', title: 'Langfuse',
description: 'LLM 애플리케이션을 디버그하고 개선하기 위한 추적, 평가, 프롬프트 관리 및 메트릭.', description:
'LLM 애플리케이션을 디버그하고 개선하기 위한 추적, 평가, 프롬프트 관리 및 메트릭.',
}, },
inUse: '사용 중', inUse: '사용 중',
configProvider: { configProvider: {
@ -157,22 +178,27 @@ const translation = {
secretKey: '비밀 키', secretKey: '비밀 키',
viewDocsLink: '{{key}} 문서 보기', viewDocsLink: '{{key}} 문서 보기',
removeConfirmTitle: '{{key}} 구성을 제거하시겠습니까?', removeConfirmTitle: '{{key}} 구성을 제거하시겠습니까?',
removeConfirmContent: '현재 구성이 사용 중입니다. 제거하면 추적 기능이 꺼집니다.', removeConfirmContent:
'현재 구성이 사용 중입니다. 제거하면 추적 기능이 꺼집니다.',
}, },
view: '보기', view: '보기',
opik: { opik: {
title: '오픽', title: '오픽',
description: 'Opik 은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.', description:
'Opik 은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.',
}, },
weave: { weave: {
title: '직조하다', title: '직조하다',
description: 'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', description:
'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.',
}, },
}, },
answerIcon: { answerIcon: {
description: 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', description:
'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부',
title: 'web app 아이콘을 사용하여 🤖', title: 'web app 아이콘을 사용하여 🤖',
descriptionInExplore: 'Explore 에서 web app 아이콘을 사용하여 바꿀🤖지 여부', descriptionInExplore:
'Explore 에서 web app 아이콘을 사용하여 바꿀🤖지 여부',
}, },
importFromDSL: 'DSL 에서 가져오기', importFromDSL: 'DSL 에서 가져오기',
importFromDSLFile: 'DSL 파일에서', importFromDSLFile: 'DSL 파일에서',
@ -211,8 +237,10 @@ const translation = {
structured: '구조화된', structured: '구조화된',
configure: '설정하다', configure: '설정하다',
moreFillTip: '최대 10 단계 중첩을 표시합니다.', moreFillTip: '최대 10 단계 중첩을 표시합니다.',
modelNotSupportedTip: '현재 모델은 이 기능을 지원하지 않으며 자동으로 프롬프트 주입으로 다운그레이드됩니다.', modelNotSupportedTip:
structuredTip: '구조화된 출력은 모델이 제공한 JSON 스키마를 항상 준수하는 응답을 생성하도록 보장하는 기능입니다.', '현재 모델은 이 기능을 지원하지 않으며 자동으로 프롬프트 주입으로 다운그레이드됩니다.',
structuredTip:
'구조화된 출력은 모델이 제공한 JSON 스키마를 항상 준수하는 응답을 생성하도록 보장하는 기능입니다.',
}, },
accessItemsDescription: { accessItemsDescription: {
anyone: '누구나 웹 앱에 접근할 수 있습니다.', anyone: '누구나 웹 앱에 접근할 수 있습니다.',
@ -240,7 +268,8 @@ const translation = {
members_one: '{{count}} 회원', members_one: '{{count}} 회원',
members_other: '{{count}} 회원', members_other: '{{count}} 회원',
noGroupsOrMembers: '선택된 그룹 또는 멤버가 없습니다.', noGroupsOrMembers: '선택된 그룹 또는 멤버가 없습니다.',
webAppSSONotEnabledTip: '웹 앱 인증 방법을 구성하려면 엔터프라이즈 관리자인에게 문의하십시오.', webAppSSONotEnabledTip:
'웹 앱 인증 방법을 구성하려면 엔터프라이즈 관리자인에게 문의하십시오.',
updateSuccess: '업데이트가 성공적으로 완료되었습니다.', updateSuccess: '업데이트가 성공적으로 완료되었습니다.',
description: '웹 앱 접근 권한 설정', description: '웹 앱 접근 권한 설정',
}, },

@ -20,21 +20,25 @@ const translation = {
save: '절약 ', save: '절약 ',
free: '무료', free: '무료',
currentPlan: '현재 요금제', currentPlan: '현재 요금제',
contractSales: '영업에 문의하기', contractSales: '영업에 문의하기',
contractOwner: '팀 관리자에게 문의하기', contractOwner: '팀 관리자에게 문의하기',
startForFree: '무료로 시작하기', startForFree: '무료로 시작하기',
getStartedWith: '시작하기 ', getStartedWith: '시작하기 ',
contactSales: '영업에 문의하기', contactSales: '영업에 문의하기',
talkToSales: '영업과 상담하기', talkToSales: '영업과 상담하기',
modelProviders: '모델 제공자', modelProviders: '모델 제공자',
teamMembers: '팀 멤버', teamMembers: '팀 멤버',
buildApps: '앱 만들기', buildApps: '앱 만들기',
vectorSpace: '벡터 공간', vectorSpace: '벡터 공간',
vectorSpaceBillingTooltip: '1MB 당 약 120 만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings 을 기반으로 추정되며 모델에 따라 다릅니다).', vectorSpaceBillingTooltip:
vectorSpaceTooltip: '벡터 공간은 LLM 이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.', '1MB 당 약 120 만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings 을 기반으로 추정되며 모델에 따라 다릅니다).',
vectorSpaceTooltip:
'벡터 공간은 LLM 이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.',
documentProcessingPriority: '문서 처리 우선순위', documentProcessingPriority: '문서 처리 우선순위',
documentProcessingPriorityTip: '더 높은 문서 처리 우선순위를 원하시면 요금제를 업그레이드하세요.', documentProcessingPriorityTip:
documentProcessingPriorityUpgrade: '더 높은 정확성과 빠른 속도로 데이터를 처리합니다.', '더 높은 문서 처리 우선순위를 원하시면 요금제를 업그레이드하세요.',
documentProcessingPriorityUpgrade:
'더 높은 정확성과 빠른 속도로 데이터를 처리합니다.',
priority: { priority: {
'standard': '표준', 'standard': '표준',
'priority': '우선', 'priority': '우선',
@ -60,31 +64,35 @@ const translation = {
workflow: '워크플로우', workflow: '워크플로우',
llmLoadingBalancing: 'LLM 로드 밸런싱', llmLoadingBalancing: 'LLM 로드 밸런싱',
bulkUpload: '문서 대량 업로드', bulkUpload: '문서 대량 업로드',
llmLoadingBalancingTooltip: '모델에 여러 API 키를 추가하여 API 속도 제한을 효과적으로 우회할 수 있습니다.', llmLoadingBalancingTooltip:
'모델에 여러 API 키를 추가하여 API 속도 제한을 효과적으로 우회할 수 있습니다.',
}, },
comingSoon: '곧 출시 예정', comingSoon: '곧 출시 예정',
member: '멤버', member: '멤버',
memberAfter: '멤버', memberAfter: '멤버',
messageRequest: { messageRequest: {
title: '메시지 크레딧', title: '메시지 크레딧',
tooltip: 'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.', tooltip:
'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.',
titlePerMonth: '{{count,number}} 메시지/월', titlePerMonth: '{{count,number}} 메시지/월',
}, },
annotatedResponse: { annotatedResponse: {
title: '주석 응답 쿼터', title: '주석 응답 쿼터',
tooltip: '수동으로 편집 및 응답 주석 달기로 앱의 사용자 정의 가능한 고품질 질의응답 기능을 제공합니다 (채팅 앱에만 해당).', tooltip:
'수동으로 편집 및 응답 주석 달기로 앱의 사용자 정의 가능한 고품질 질의응답 기능을 제공합니다 (채팅 앱에만 해당).',
}, },
ragAPIRequestTooltip: 'Dify 의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.', ragAPIRequestTooltip:
'Dify 의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.',
receiptInfo: '팀 소유자 및 팀 관리자만 구독 및 청구 정보를 볼 수 있습니다', receiptInfo: '팀 소유자 및 팀 관리자만 구독 및 청구 정보를 볼 수 있습니다',
annotationQuota: 'Annotation Quota(주석 할당량)', annotationQuota: 'Annotation Quota(주석 할당량)',
documentsUploadQuota: '문서 업로드 할당량', documentsUploadQuota: '문서 업로드 할당량',
freeTrialTipPrefix: '가입하고 받으세요', freeTrialTipPrefix: '요금제에 가입하고 ',
comparePlanAndFeatures: '계획 및 기능 비교', comparePlanAndFeatures: '계획 및 기능 비교',
documents: '{{count,number}} 지식 문서', documents: '{{count,number}} 지식 문서',
apiRateLimit: 'API 요금 한도', apiRateLimit: 'API 요금 한도',
cloud: '클라우드 서비스', cloud: '클라우드 서비스',
unlimitedApiRate: 'API 호출 속도 제한 없음', unlimitedApiRate: 'API 호출 속도 제한 없음',
freeTrialTip: '200 회의 OpenAI 호출에 대한 무료 체험.', freeTrialTip: '200 회의 OpenAI 호출 무료 체험을 받으세요. ',
annualBilling: '연간 청구', annualBilling: '연간 청구',
getStarted: '시작하기', getStarted: '시작하기',
apiRateLimitUnit: '{{count,number}}/일', apiRateLimitUnit: '{{count,number}}/일',
@ -94,10 +102,13 @@ const translation = {
teamMember_other: '{{count,number}} 팀원', teamMember_other: '{{count,number}} 팀원',
teamMember_one: '{{count,number}} 팀원', teamMember_one: '{{count,number}} 팀원',
priceTip: '작업 공간당/', priceTip: '작업 공간당/',
apiRateLimitTooltip: 'Dify API 를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.', apiRateLimitTooltip:
'Dify API 를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.',
documentsRequestQuota: '{{count,number}}/분 지식 요청 비율 제한', documentsRequestQuota: '{{count,number}}/분 지식 요청 비율 제한',
documentsTooltip: '지식 데이터 소스에서 가져올 수 있는 문서 수에 대한 쿼터.', documentsTooltip:
documentsRequestQuotaTooltip: '지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1 분 이내에 10 회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1 분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.', '지식 데이터 소스에서 가져올 수 있는 문서 수에 대한 쿼터.',
documentsRequestQuotaTooltip:
'지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1 분 이내에 10 회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1 분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.',
}, },
plans: { plans: {
sandbox: { sandbox: {
@ -108,9 +119,10 @@ const translation = {
}, },
professional: { professional: {
name: '프로페셔널', name: '프로페셔널',
description: '개인 및 소규모 팀을 위해 더 많은 파워를 저렴한 가격에 제공합니다.', description:
'개인 및 소규모 팀을 위해 더 많은 파워를 저렴한 가격에 제공합니다.',
includesTitle: '무료 플랜에 추가로 포함된 항목:', includesTitle: '무료 플랜에 추가로 포함된 항목:',
for: '독립 개발자/소규모 팀을 위한', for: '1인 개발자/소규모 팀을 위한',
}, },
team: { team: {
name: '팀', name: '팀',
@ -120,7 +132,8 @@ const translation = {
}, },
enterprise: { enterprise: {
name: '엔터프라이즈', name: '엔터프라이즈',
description: '대규모 미션 크리티컬 시스템을 위한 완전한 기능과 지원을 제공합니다.', description:
'대규모 미션 크리티컬 시스템을 위한 완전한 기능과 지원을 제공합니다.',
includesTitle: '팀 플랜에 추가로 포함된 항목:', includesTitle: '팀 플랜에 추가로 포함된 항목:',
features: { features: {
2: '독점 기업 기능', 2: '독점 기업 기능',
@ -178,7 +191,8 @@ const translation = {
contactUs: '문의하기', contactUs: '문의하기',
fullTip1: '업그레이드하여 더 많은 앱을 만들기', fullTip1: '업그레이드하여 더 많은 앱을 만들기',
fullTip2: '계획 한도에 도달했습니다.', fullTip2: '계획 한도에 도달했습니다.',
fullTip2des: '비활성 애플리케이션을 정리하여 사용량을 줄이거나 저희에게 문의하는 것이 좋습니다.', fullTip2des:
'비활성 애플리케이션을 정리하여 사용량을 줄이거나 저희에게 문의하는 것이 좋습니다.',
fullTip1des: '이 계획에서 앱을 구축할 수 있는 한계에 도달했습니다.', fullTip1des: '이 계획에서 앱을 구축할 수 있는 한계에 도달했습니다.',
}, },
annotatedResponse: { annotatedResponse: {
@ -192,7 +206,8 @@ const translation = {
teamMembers: '팀원들', teamMembers: '팀원들',
buildApps: '앱 만들기', buildApps: '앱 만들기',
documentsUploadQuota: '문서 업로드 한도', documentsUploadQuota: '문서 업로드 한도',
vectorSpaceTooltip: '고품질 색인 모드를 사용하는 문서는 지식 데이터 저장소 자원을 소모합니다. 지식 데이터 저장소가 한도에 도달하면 새 문서를 업로드할 수 없습니다.', vectorSpaceTooltip:
'고품질 색인 모드를 사용하는 문서는 지식 데이터 저장소 자원을 소모합니다. 지식 데이터 저장소가 한도에 도달하면 새 문서를 업로드할 수 없습니다.',
}, },
teamMembers: '팀원들', teamMembers: '팀원들',
} }

@ -28,6 +28,8 @@ const translation = {
delete: '삭제', delete: '삭제',
enableWarning: '아카이브된 파일은 활성화할 수 없습니다.', enableWarning: '아카이브된 파일은 활성화할 수 없습니다.',
sync: '동기화', sync: '동기화',
resume: '이력서',
pause: '일시 중지',
}, },
index: { index: {
enable: '활성화', enable: '활성화',

@ -2,7 +2,8 @@ const translation = {
toVerifiedTip: { toVerifiedTip: {
end: 'Dify 프로페셔널 플랜을 위해.', end: 'Dify 프로페셔널 플랜을 위해.',
coupon: '독점 100% 쿠폰', coupon: '독점 100% 쿠폰',
front: '당신은 이제 교육 인증 상태를 받을 자격이 있습니다. 아래에 귀하의 교육 정보를 입력하여 과정을 완료하고 인증을 받으십시오.', front:
'당신은 이제 교육 인증 상태를 받을 자격이 있습니다. 아래에 귀하의 교육 정보를 입력하여 과정을 완료하고 인증을 받으십시오.',
}, },
form: { form: {
schoolName: { schoolName: {
@ -26,15 +27,18 @@ const translation = {
privacyPolicy: '개인정보 보호정책', privacyPolicy: '개인정보 보호정책',
}, },
option: { option: {
inSchool: '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify 는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.', inSchool:
age: '나는 최소한 18 세 이상임을 확인합니다.', '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify 는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.',
age: '만 18세 이상입니다.',
}, },
title: '약관 및 동의사항', title: '약관 및 동의사항',
}, },
}, },
submit: '제출', submit: '제출',
rejectContent: '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.', rejectContent:
successContent: '귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1 년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.', '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.',
successContent:
'귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1 년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.',
currentSigned: '현재 로그인 중입니다', currentSigned: '현재 로그인 중입니다',
toVerified: '교육 인증 받기', toVerified: '교육 인증 받기',
rejectTitle: '귀하의 Dify 교육 인증이 거부되었습니다.', rejectTitle: '귀하의 Dify 교육 인증이 거부되었습니다.',

@ -16,7 +16,7 @@ const translation = {
image: '이미지', image: '이미지',
design: '디자인', design: '디자인',
business: '사업', business: '사업',
agent: '대리인', agent: '에이전트',
}, },
allTags: '모든 태그', allTags: '모든 태그',
searchTags: '검색 태그', searchTags: '검색 태그',

@ -1,12 +1,12 @@
const translation = { const translation = {
daysInWeek: { daysInWeek: {
Sun: '일요일',
Mon: '월요일',
Tue: '화요일',
Wed: '수요일', Wed: '수요일',
Thu: '목요일', Thu: '목요일',
Fri: '자유', Fri: '금요일',
Sat: '토요일', Sat: '토요일',
Sun: '태양',
Tue: '화요일',
Mon: '몬',
}, },
months: { months: {
May: '5 월', May: '5 월',
@ -26,7 +26,7 @@ const translation = {
pickDate: '날짜 선택', pickDate: '날짜 선택',
cancel: '취소', cancel: '취소',
ok: '좋아요', ok: '좋아요',
now: '지금', now: '오늘',
}, },
title: { title: {
pickTime: '시간 선택', pickTime: '시간 선택',

@ -4,9 +4,9 @@ const translation = {
redo: '다시 실행', redo: '다시 실행',
editing: '편집 중', editing: '편집 중',
autoSaved: '자동 저장됨', autoSaved: '자동 저장됨',
unpublished: '미발행', unpublished: '게시되지 않음',
published: '발행됨', published: '게시됨',
publish: '발행', publish: '게시하기',
update: '업데이트', update: '업데이트',
run: '실행', run: '실행',
running: '실행 중', running: '실행 중',
@ -43,7 +43,8 @@ const translation = {
previewPlaceholder: '디버깅을 시작하려면 아래 상자에 내용을 입력하세요', previewPlaceholder: '디버깅을 시작하려면 아래 상자에 내용을 입력하세요',
effectVarConfirm: { effectVarConfirm: {
title: '변수 제거', title: '변수 제거',
content: '변수가 다른 노드에서 사용되고 있습니다. 그래도 제거하시겠습니까?', content:
'변수가 다른 노드에서 사용되고 있습니다. 그래도 제거하시겠습니까?',
}, },
insertVarTip: '빠르게 삽입하려면 \'/\' 키를 누르세요', insertVarTip: '빠르게 삽입하려면 \'/\' 키를 누르세요',
processData: '데이터 처리', processData: '데이터 처리',
@ -58,7 +59,7 @@ const translation = {
duplicate: '복제', duplicate: '복제',
pasteHere: '여기에 붙여넣기', pasteHere: '여기에 붙여넣기',
pointerMode: '포인터 모드', pointerMode: '포인터 모드',
handMode: '드 모드', handMode: '래그 모드',
model: '모델', model: '모델',
workflowAsTool: '도구로서의 워크플로우', workflowAsTool: '도구로서의 워크플로우',
configureRequired: '구성 필요', configureRequired: '구성 필요',
@ -73,7 +74,8 @@ const translation = {
overwriteAndImport: '덮어쓰기 및 가져오기', overwriteAndImport: '덮어쓰기 및 가져오기',
importSuccess: '가져오기 성공', importSuccess: '가져오기 성공',
syncingData: '단 몇 초 만에 데이터를 동기화할 수 있습니다.', syncingData: '단 몇 초 만에 데이터를 동기화할 수 있습니다.',
importDSLTip: '현재 초안을 덮어씁니다. 가져오기 전에 워크플로를 백업으로 내보냅니다.', importDSLTip:
'현재 초안을 덮어씁니다. 가져오기 전에 워크플로우를 백업으로 내보냅니다.',
parallelTip: { parallelTip: {
click: { click: {
title: '클릭', title: '클릭',
@ -95,9 +97,11 @@ const translation = {
featuresDocLink: '더 알아보세요', featuresDocLink: '더 알아보세요',
fileUploadTip: '이미지 업로드 기능이 파일 업로드로 업그레이드되었습니다.', fileUploadTip: '이미지 업로드 기능이 파일 업로드로 업그레이드되었습니다.',
featuresDescription: '웹앱 사용자 경험 향상', featuresDescription: '웹앱 사용자 경험 향상',
ImageUploadLegacyTip: '이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.', ImageUploadLegacyTip:
'이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.',
importWarning: '주의', importWarning: '주의',
importWarningDetails: 'DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', importWarningDetails:
'DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.',
openInExplore: 'Explore 에서 열기', openInExplore: 'Explore 에서 열기',
onFailure: '실패 시', onFailure: '실패 시',
addFailureBranch: '실패 분기 추가', addFailureBranch: '실패 분기 추가',
@ -118,7 +122,8 @@ const translation = {
}, },
env: { env: {
envPanelTitle: '환경 변수', envPanelTitle: '환경 변수',
envDescription: '환경 변수는 개인 정보와 자격 증명을 저장하는 데 사용될 수 있습니다. 이들은 읽기 전용이며 내보내기 중에 DSL 파일과 분리할 수 있습니다.', envDescription:
'환경 변수는 개인 정보와 자격 증명을 저장하는 데 사용될 수 있습니다. 이들은 읽기 전용이며 내보내기 중에 DSL 파일과 분리할 수 있습니다.',
envPanelButton: '변수 추가', envPanelButton: '변수 추가',
modal: { modal: {
title: '환경 변수 추가', title: '환경 변수 추가',
@ -128,7 +133,8 @@ const translation = {
namePlaceholder: '환경 이름', namePlaceholder: '환경 이름',
value: '값', value: '값',
valuePlaceholder: '환경 값', valuePlaceholder: '환경 값',
secretTip: '민감한 정보나 데이터를 정의하는 데 사용되며, DSL 설정은 유출 방지를 위해 구성됩니다.', secretTip:
'민감한 정보나 데이터를 정의하는 데 사용되며, DSL 설정은 유출 방지를 위해 구성됩니다.',
description: '설명', description: '설명',
descriptionPlaceholder: '변수에 대해 설명하세요', descriptionPlaceholder: '변수에 대해 설명하세요',
}, },
@ -141,7 +147,8 @@ const translation = {
}, },
chatVariable: { chatVariable: {
panelTitle: '대화 변수', panelTitle: '대화 변수',
panelDescription: '대화 변수는 LLM 이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.', panelDescription:
'대화 변수는 LLM 이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.',
docLink: '자세한 내용은 문서를 참조하세요.', docLink: '자세한 내용은 문서를 참조하세요.',
button: '변수 추가', button: '변수 추가',
modal: { modal: {
@ -171,7 +178,8 @@ const translation = {
placeholder: '아직 아무 것도 변경하지 않았습니다', placeholder: '아직 아무 것도 변경하지 않았습니다',
clearHistory: '기록 지우기', clearHistory: '기록 지우기',
hint: '힌트', hint: '힌트',
hintText: '편집 작업이 변경 기록에 추적되며, 이 세션 동안 기기에 저장됩니다. 편집기를 떠나면 이 기록이 지워집니다.', hintText:
'편집 작업이 변경 기록에 추적되며, 이 세션 동안 기기에 저장됩니다. 편집기를 떠나면 이 기록이 지워집니다.',
stepBackward_one: '{{count}} 단계 뒤로', stepBackward_one: '{{count}} 단계 뒤로',
stepBackward_other: '{{count}} 단계 뒤로', stepBackward_other: '{{count}} 단계 뒤로',
stepForward_one: '{{count}} 단계 앞으로', stepForward_one: '{{count}} 단계 앞으로',
@ -205,7 +213,8 @@ const translation = {
visionVariable: '비전 변수', visionVariable: '비전 변수',
}, },
invalidVariable: '잘못된 변수', invalidVariable: '잘못된 변수',
rerankModelRequired: 'Rerank Model 을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.', rerankModelRequired:
'Rerank Model 을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.',
noValidTool: '{{field}} 유효한 도구가 선택되지 않았습니다.', noValidTool: '{{field}} 유효한 도구가 선택되지 않았습니다.',
toolParameterRequired: '{{field}}: 매개변수 [{{param}}] 이 필요합니다.', toolParameterRequired: '{{field}}: 매개변수 [{{param}}] 이 필요합니다.',
}, },
@ -264,22 +273,34 @@ const translation = {
'end': '워크플로우의 종료 및 결과 유형을 정의합니다', 'end': '워크플로우의 종료 및 결과 유형을 정의합니다',
'answer': '대화의 답변 내용을 정의합니다', 'answer': '대화의 답변 내용을 정의합니다',
'llm': '질문에 답하거나 자연어를 처리하기 위해 대형 언어 모델을 호출합니다', 'llm': '질문에 답하거나 자연어를 처리하기 위해 대형 언어 모델을 호출합니다',
'knowledge-retrieval': '사용자 질문과 관련된 텍스트 콘텐츠를 지식 베이스에서 쿼리할 수 있습니다', 'knowledge-retrieval':
'question-classifier': '사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다', '사용자 질문과 관련된 텍스트 콘텐츠를 지식 베이스에서 쿼리할 수 있습니다',
'if-else': 'if/else 조건을 기반으로 워크플로우를 두 가지 분기로 나눌 수 있습니다', 'question-classifier':
'사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다',
'if-else':
'if/else 조건을 기반으로 워크플로우를 두 가지 분기로 나눌 수 있습니다',
'code': '사용자 정의 논리를 구현하기 위해 Python 또는 NodeJS 코드를 실행합니다', 'code': '사용자 정의 논리를 구현하기 위해 Python 또는 NodeJS 코드를 실행합니다',
'template-transform': 'Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다', 'template-transform':
'Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다',
'http-request': 'HTTP 프로토콜을 통해 서버 요청을 보낼 수 있습니다', 'http-request': 'HTTP 프로토콜을 통해 서버 요청을 보낼 수 있습니다',
'variable-assigner': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', 'variable-assigner':
'assigner': '변수 할당 노드는 쓰기 가능한 변수 (대화 변수 등) 에 값을 할당하는 데 사용됩니다.', '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.',
'variable-aggregator': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', 'assigner':
'iteration': '목록 객체에서 여러 단계를 수행하여 모든 결과가 출력될 때까지 반복합니다.', '변수 할당 노드는 쓰기 가능한 변수 (대화 변수 등) 에 값을 할당하는 데 사용됩니다.',
'parameter-extractor': '도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.', 'variable-aggregator':
'document-extractor': '업로드된 문서를 LLM 에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.', '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.',
'iteration':
'목록 객체에서 여러 단계를 수행하여 모든 결과가 출력될 때까지 반복합니다.',
'parameter-extractor':
'도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.',
'document-extractor':
'업로드된 문서를 LLM 에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.',
'list-operator': '배열 내용을 필터링하거나 정렬하는 데 사용됩니다.', 'list-operator': '배열 내용을 필터링하거나 정렬하는 데 사용됩니다.',
'agent': '질문에 답하거나 자연어를 처리하기 위해 대규모 언어 모델을 호출하는 경우', 'agent':
'질문에 답하거나 자연어를 처리하기 위해 대규모 언어 모델을 호출하는 경우',
'loop': '종료 조건이 충족되거나 최대 반복 횟수에 도달할 때까지 논리 루프를 실행합니다.', 'loop': '종료 조건이 충족되거나 최대 반복 횟수에 도달할 때까지 논리 루프를 실행합니다.',
'loop-end': '"break"와 동일합니다. 이 노드는 구성 항목이 없습니다. 루프 본문이 이 노드에 도달하면 루프가 종료됩니다.', 'loop-end':
'"break"와 동일합니다. 이 노드는 구성 항목이 없습니다. 루프 본문이 이 노드에 도달하면 루프가 종료됩니다.',
}, },
operator: { operator: {
zoomIn: '확대', zoomIn: '확대',
@ -306,7 +327,7 @@ const translation = {
changeBlock: '노드 변경', changeBlock: '노드 변경',
addNextStep: '이 워크플로우에 다음 단계를 추가하세요.', addNextStep: '이 워크플로우에 다음 단계를 추가하세요.',
minimize: '전체 화면 종료', minimize: '전체 화면 종료',
maximize: '캔버스를 최대화하다', maximize: '캔버스 전체 화면',
}, },
nodes: { nodes: {
common: { common: {
@ -340,9 +361,12 @@ const translation = {
failBranch: { failBranch: {
title: '실패 분기', title: '실패 분기',
desc: '오류가 발생하면 예외 분기를 실행합니다', desc: '오류가 발생하면 예외 분기를 실행합니다',
customize: '캔버스로 이동하여 fail branch logic 를 사용자 지정합니다.', customize:
inLog: '노드 예외는 실패 분기를 자동으로 실행합니다. 노드 출력은 오류 유형 및 오류 메시지를 반환하고 다운스트림으로 전달합니다.', '캔버스로 이동하여 fail branch logic 를 사용자 지정합니다.',
customizeTip: 'fail 분기가 활성화되면 노드에서 throw 된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.', inLog:
'노드 예외는 실패 분기를 자동으로 실행합니다. 노드 출력은 오류 유형 및 오류 메시지를 반환하고 다운스트림으로 전달합니다.',
customizeTip:
'fail 분기가 활성화되면 노드에서 throw 된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.',
}, },
partialSucceeded: { partialSucceeded: {
tip: '프로세스에 {{num}} 노드가 비정상적으로 실행 중입니다. 추적으로 이동하여 로그를 확인하십시오.', tip: '프로세스에 {{num}} 노드가 비정상적으로 실행 중입니다. 추적으로 이동하여 로그를 확인하십시오.',
@ -360,7 +384,7 @@ const translation = {
retrySuccessful: '재시도 성공', retrySuccessful: '재시도 성공',
retryFailed: '재시도 실패', retryFailed: '재시도 실패',
retryFailedTimes: '{{times}} 재시도 실패', retryFailedTimes: '{{times}} 재시도 실패',
times: '', times: '',
ms: '미에스', ms: '미에스',
retries: '{{숫자}} 재시도', retries: '{{숫자}} 재시도',
}, },
@ -401,7 +425,8 @@ const translation = {
variables: '변수', variables: '변수',
context: '컨텍스트', context: '컨텍스트',
contextTooltip: '컨텍스트로 지식을 가져올 수 있습니다', contextTooltip: '컨텍스트로 지식을 가져올 수 있습니다',
notSetContextInPromptTip: '컨텍스트 기능을 활성화하려면 PROMPT 에 컨텍스트 변수를 입력하세요.', notSetContextInPromptTip:
'컨텍스트 기능을 활성화하려면 PROMPT 에 컨텍스트 변수를 입력하세요.',
prompt: '프롬프트', prompt: '프롬프트',
roleDescription: { roleDescription: {
system: '대화를 위한 고급 지침 제공', system: '대화를 위한 고급 지침 제공',
@ -445,8 +470,10 @@ const translation = {
stringValidations: '문자열 검증', stringValidations: '문자열 검증',
showAdvancedOptions: '고급 옵션 표시', showAdvancedOptions: '고급 옵션 표시',
promptPlaceholder: '당신의 JSON 스키마를 설명하세요...', promptPlaceholder: '당신의 JSON 스키마를 설명하세요...',
generationTip: '자연어를 사용하여 JSON 스키마를 신속하게 생성할 수 있습니다.', generationTip:
resultTip: '여기 생성된 결과가 있습니다. 만약 만족하지 않으신다면, 돌아가서 프롬프트를 수정할 수 있습니다.', '자연어를 사용하여 JSON 스키마를 신속하게 생성할 수 있습니다.',
resultTip:
'여기 생성된 결과가 있습니다. 만약 만족하지 않으신다면, 돌아가서 프롬프트를 수정할 수 있습니다.',
regenerate: '재생하다', regenerate: '재생하다',
required: '필수', required: '필수',
doc: '구조화된 출력에 대해 더 알아보세요.', doc: '구조화된 출력에 대해 더 알아보세요.',
@ -472,7 +499,8 @@ const translation = {
}, },
automatic: { automatic: {
desc: '쿼리 변수를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.', desc: '쿼리 변수를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.',
subTitle: '사용자 쿼리를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.', subTitle:
'사용자 쿼리를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.',
title: '자동', title: '자동',
}, },
manual: { manual: {
@ -544,7 +572,8 @@ const translation = {
inputVars: '입력 변수', inputVars: '입력 변수',
outputVars: '출력 변수', outputVars: '출력 변수',
advancedDependencies: '고급 종속성', advancedDependencies: '고급 종속성',
advancedDependenciesTip: '더 많은 시간이 소요되거나 기본으로 내장되지 않은 일부 미리 로드된 종속성을 여기에 추가하세요', advancedDependenciesTip:
'더 많은 시간이 소요되거나 기본으로 내장되지 않은 일부 미리 로드된 종속성을 여기에 추가하세요',
searchDependencies: '종속성 검색', searchDependencies: '종속성 검색',
}, },
templateTransform: { templateTransform: {
@ -558,7 +587,8 @@ const translation = {
ifElse: { ifElse: {
if: 'If', if: 'If',
else: 'Else', else: 'Else',
elseDescription: 'If 조건이 충족되지 않을 때 실행할 논리를 정의하는 데 사용됩니다.', elseDescription:
'If 조건이 충족되지 않을 때 실행할 논리를 정의하는 데 사용됩니다.',
and: '그리고', and: '그리고',
or: '또는', or: '또는',
operator: '연산자', operator: '연산자',
@ -611,7 +641,8 @@ const translation = {
array: '배열', array: '배열',
}, },
aggregationGroup: '집계 그룹', aggregationGroup: '집계 그룹',
aggregationGroupTip: '이 기능을 활성화하면 변수 집계자가 여러 변수 집합을 집계할 수 있습니다.', aggregationGroupTip:
'이 기능을 활성화하면 변수 집계자가 여러 변수 집합을 집계할 수 있습니다.',
addGroup: '그룹 추가', addGroup: '그룹 추가',
outputVars: { outputVars: {
varDescribe: '{{groupName}} 출력', varDescribe: '{{groupName}} 출력',
@ -647,7 +678,8 @@ const translation = {
'noAssignedVars': '사용 가능한 할당된 변수가 없습니다.', 'noAssignedVars': '사용 가능한 할당된 변수가 없습니다.',
'noVarTip': '"+" 버튼을 클릭하여 변수를 추가합니다.', 'noVarTip': '"+" 버튼을 클릭하여 변수를 추가합니다.',
'setParameter': '매개 변수 설정...', 'setParameter': '매개 변수 설정...',
'assignedVarsDescription': '할당된 변수는 대화 변수와 같은 쓰기 가능한 변수여야 합니다.', 'assignedVarsDescription':
'할당된 변수는 대화 변수와 같은 쓰기 가능한 변수여야 합니다.',
'selectAssignedVariable': '할당된 변수 선택...', 'selectAssignedVariable': '할당된 변수 선택...',
'varNotSet': '변수가 설정되지 않음', 'varNotSet': '변수가 설정되지 않음',
}, },
@ -679,7 +711,8 @@ const translation = {
topicPlaceholder: '주제 이름을 작성하세요', topicPlaceholder: '주제 이름을 작성하세요',
addClass: '클래스 추가', addClass: '클래스 추가',
instruction: '지시', instruction: '지시',
instructionTip: '질문 분류기가 질문을 더 잘 분류할 수 있도록 추가 지시를 입력하세요.', instructionTip:
'질문 분류기가 질문을 더 잘 분류할 수 있도록 추가 지시를 입력하세요.',
instructionPlaceholder: '지시를 작성하세요', instructionPlaceholder: '지시를 작성하세요',
}, },
parameterExtractor: { parameterExtractor: {
@ -695,14 +728,17 @@ const translation = {
description: '설명', description: '설명',
descriptionPlaceholder: '추출 매개변수 설명', descriptionPlaceholder: '추출 매개변수 설명',
required: '필수', required: '필수',
requiredContent: '필수는 모델 추론을 위한 참고 용도로만 사용되며, 매개변수 출력의 필수 유효성 검사는 아닙니다.', requiredContent:
'필수는 모델 추론을 위한 참고 용도로만 사용되며, 매개변수 출력의 필수 유효성 검사는 아닙니다.',
}, },
extractParametersNotSet: '추출 매개변수가 설정되지 않음', extractParametersNotSet: '추출 매개변수가 설정되지 않음',
instruction: '지시', instruction: '지시',
instructionTip: '매개변수 추출기가 매개변수를 추출하는 방법을 이해하는 데 도움이 되는 추가 지시를 입력하세요.', instructionTip:
'매개변수 추출기가 매개변수를 추출하는 방법을 이해하는 데 도움이 되는 추가 지시를 입력하세요.',
advancedSetting: '고급 설정', advancedSetting: '고급 설정',
reasoningMode: '추론 모드', reasoningMode: '추론 모드',
reasoningModeTip: '모델의 함수 호출 또는 프롬프트에 대한 지시 응답 능력을 기반으로 적절한 추론 모드를 선택할 수 있습니다.', reasoningModeTip:
'모델의 함수 호출 또는 프롬프트에 대한 지시 응답 능력을 기반으로 적절한 추론 모드를 선택할 수 있습니다.',
isSuccess: '성공 여부. 성공 시 값은 1 이고, 실패 시 값은 0 입니다.', isSuccess: '성공 여부. 성공 시 값은 1 이고, 실패 시 값은 0 입니다.',
errorReason: '오류 원인', errorReason: '오류 원인',
}, },
@ -728,9 +764,12 @@ const translation = {
error_other: '{{개수}} 오류', error_other: '{{개수}} 오류',
parallelModeEnableTitle: 'Parallel Mode Enabled(병렬 모드 사용)', parallelModeEnableTitle: 'Parallel Mode Enabled(병렬 모드 사용)',
parallelPanelDesc: '병렬 모드에서 반복의 작업은 병렬 실행을 지원합니다.', parallelPanelDesc: '병렬 모드에서 반복의 작업은 병렬 실행을 지원합니다.',
parallelModeEnableDesc: '병렬 모드에서는 반복 내의 작업이 병렬 실행을 지원합니다. 오른쪽의 속성 패널에서 이를 구성할 수 있습니다.', parallelModeEnableDesc:
MaxParallelismDesc: '최대 병렬 처리는 단일 반복에서 동시에 실행되는 작업 수를 제어하는 데 사용됩니다.', '병렬 모드에서는 반복 내의 작업이 병렬 실행을 지원합니다. 오른쪽의 속성 패널에서 이를 구성할 수 있습니다.',
answerNodeWarningDesc: '병렬 모드 경고: 응답 노드, 대화 변수 할당 및 반복 내의 지속적인 읽기/쓰기 작업으로 인해 예외가 발생할 수 있습니다.', MaxParallelismDesc:
'최대 병렬 처리는 단일 반복에서 동시에 실행되는 작업 수를 제어하는 데 사용됩니다.',
answerNodeWarningDesc:
'병렬 모드 경고: 응답 노드, 대화 변수 할당 및 반복 내의 지속적인 읽기/쓰기 작업으로 인해 예외가 발생할 수 있습니다.',
}, },
note: { note: {
editor: { editor: {
@ -780,12 +819,14 @@ const translation = {
agent: { agent: {
strategy: { strategy: {
label: '에이전트 전략', label: '에이전트 전략',
tooltip: '다양한 에이전트 전략은 시스템이 다단계 도구 호출을 계획하고 실행하는 방법을 결정합니다', tooltip:
'다양한 에이전트 전략은 시스템이 다단계 도구 호출을 계획하고 실행하는 방법을 결정합니다',
configureTip: '에이전트 전략을 구성하세요.', configureTip: '에이전트 전략을 구성하세요.',
searchPlaceholder: '검색 에이전트 전략', searchPlaceholder: '검색 에이전트 전략',
shortLabel: '전략', shortLabel: '전략',
selectTip: '에이전트 전략 선택', selectTip: '에이전트 전략 선택',
configureTipDesc: '에이전트 전략을 구성한 후 이 노드는 나머지 구성을 자동으로 로드합니다. 이 전략은 다단계 도구 추론의 메커니즘에 영향을 미칩니다.', configureTipDesc:
'에이전트 전략을 구성한 후 이 노드는 나머지 구성을 자동으로 로드합니다. 이 전략은 다단계 도구 추론의 메커니즘에 영향을 미칩니다.',
}, },
pluginInstaller: { pluginInstaller: {
install: '설치하다', install: '설치하다',
@ -798,7 +839,8 @@ const translation = {
}, },
modelNotSupport: { modelNotSupport: {
title: '지원되지 않는 모델', title: '지원되지 않는 모델',
descForVersionSwitch: '설치된 플러그인 버전은 이 모델을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', descForVersionSwitch:
'설치된 플러그인 버전은 이 모델을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.',
desc: '설치된 플러그인 버전은 이 모델을 제공하지 않습니다.', desc: '설치된 플러그인 버전은 이 모델을 제공하지 않습니다.',
}, },
modelSelectorTooltips: { modelSelectorTooltips: {
@ -825,13 +867,17 @@ const translation = {
cancel: '취소', cancel: '취소',
title: '플러그인 설치', title: '플러그인 설치',
}, },
strategyNotFoundDescAndSwitchVersion: '설치된 플러그인 버전은 이 전략을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', strategyNotFoundDescAndSwitchVersion:
'설치된 플러그인 버전은 이 전략을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.',
learnMore: '더 알아보세요', learnMore: '더 알아보세요',
toolNotAuthorizedTooltip: '{{도구}} 권한이 부여되지 않음', toolNotAuthorizedTooltip: '{{도구}} 권한이 부여되지 않음',
strategyNotFoundDesc: '설치된 플러그인 버전은 이 전략을 제공하지 않습니다.', strategyNotFoundDesc:
'설치된 플러그인 버전은 이 전략을 제공하지 않습니다.',
maxIterations: '최대 반복 횟수', maxIterations: '최대 반복 횟수',
pluginNotFoundDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', pluginNotFoundDesc:
pluginNotInstalledDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.',
pluginNotInstalledDesc:
'이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.',
strategyNotInstallTooltip: '{{strategy}}가 설치되지 않았습니다.', strategyNotInstallTooltip: '{{strategy}}가 설치되지 않았습니다.',
tools: '도구', tools: '도구',
unsupportedStrategy: '지원되지 않는 전략', unsupportedStrategy: '지원되지 않는 전략',
@ -870,9 +916,11 @@ const translation = {
loopVariables: '루프 변수', loopVariables: '루프 변수',
setLoopVariables: '루프 범위 내에서 변수를 설정합니다.', setLoopVariables: '루프 범위 내에서 변수를 설정합니다.',
initialLoopVariables: '초기 루프 변수', initialLoopVariables: '초기 루프 변수',
breakConditionTip: '종료 조건과 대화 변수가 있는 루프 내에서만 변수를 참조할 수 있습니다.', breakConditionTip:
'종료 조건과 대화 변수가 있는 루프 내에서만 변수를 참조할 수 있습니다.',
currentLoopCount: '현재 루프 카운트: {{count}}', currentLoopCount: '현재 루프 카운트: {{count}}',
loopMaxCountError: '유효한 최대 루프 수를 입력하십시오. 범위는 1 에서 {{maxCount}}입니다.', loopMaxCountError:
'유효한 최대 루프 수를 입력하십시오. 범위는 1 에서 {{maxCount}}입니다.',
totalLoopCount: '총 루프 횟수: {{count}}', totalLoopCount: '총 루프 횟수: {{count}}',
variableName: '변수 이름', variableName: '변수 이름',
loopNode: '루프 노드', loopNode: '루프 노드',
@ -887,7 +935,8 @@ const translation = {
conversationVars: '대화 변수', conversationVars: '대화 변수',
noVarsForOperation: '선택한 작업에 할당할 수 있는 변수가 없습니다.', noVarsForOperation: '선택한 작업에 할당할 수 있는 변수가 없습니다.',
noAssignedVars: '사용 가능한 할당된 변수가 없습니다.', noAssignedVars: '사용 가능한 할당된 변수가 없습니다.',
assignedVarsDescription: '할당된 변수는 다음과 같이 쓰기 가능한 변수여야 합니다.', assignedVarsDescription:
'할당된 변수는 다음과 같이 쓰기 가능한 변수여야 합니다.',
}, },
versionHistory: { versionHistory: {
filter: { filter: {
@ -901,7 +950,8 @@ const translation = {
titleLengthLimit: '제목은 {{limit}}자를 초과할 수 없습니다.', titleLengthLimit: '제목은 {{limit}}자를 초과할 수 없습니다.',
title: '제목', title: '제목',
releaseNotes: '릴리스 노트', releaseNotes: '릴리스 노트',
releaseNotesLengthLimit: '릴리스 노트는 {{limit}}자를 초과할 수 없습니다.', releaseNotesLengthLimit:
'릴리스 노트는 {{limit}}자를 초과할 수 없습니다.',
}, },
action: { action: {
updateFailure: '버전 업데이트에 실패했습니다.', updateFailure: '버전 업데이트에 실패했습니다.',
@ -916,8 +966,8 @@ const translation = {
currentDraft: '현재 초안', currentDraft: '현재 초안',
releaseNotesPlaceholder: '변경된 내용을 설명하세요.', releaseNotesPlaceholder: '변경된 내용을 설명하세요.',
defaultName: '제목 없는 버전', defaultName: '제목 없는 버전',
nameThisVersion: '이 버전의 이름을 지어주세요', nameThisVersion: '이름 바꾸기',
title: '버전', title: '버전 기록',
deletionTip: '삭제는 되돌릴 수 없으니, 확인해 주시기 바랍니다.', deletionTip: '삭제는 되돌릴 수 없으니, 확인해 주시기 바랍니다.',
restorationTip: '버전 복원 후 현재 초안이 덮어쓰여질 것입니다.', restorationTip: '버전 복원 후 현재 초안이 덮어쓰여질 것입니다.',
}, },
@ -945,7 +995,8 @@ const translation = {
clearNode: '캐시된 변수를 지우기', clearNode: '캐시된 변수를 지우기',
resetConversationVar: '대화 변수를 기본 값으로 재설정합니다.', resetConversationVar: '대화 변수를 기본 값으로 재설정합니다.',
reset: '마지막 실행 값으로 재설정', reset: '마지막 실행 값으로 재설정',
emptyTip: '캔버스에서 노드를 한 단계씩 실행한 후, 변수 검사에서 노드 변수의 현재 값을 볼 수 있습니다.', emptyTip:
'캔버스에서 노드를 한 단계씩 실행한 후, 변수 검사에서 노드 변수의 현재 값을 볼 수 있습니다.',
}, },
settingsTab: '설정', settingsTab: '설정',
lastRunTab: '마지막 실행', lastRunTab: '마지막 실행',

@ -28,6 +28,8 @@ const translation = {
delete: 'Usuń', delete: 'Usuń',
enableWarning: 'Zarchiwizowany plik nie może zostać włączony', enableWarning: 'Zarchiwizowany plik nie może zostać włączony',
sync: 'Synchronizuj', sync: 'Synchronizuj',
resume: 'Wznawiać',
pause: 'Pauza',
}, },
index: { index: {
enable: 'Włącz', enable: 'Włącz',

@ -28,6 +28,8 @@ const translation = {
delete: 'Excluir', delete: 'Excluir',
enableWarning: 'O arquivo arquivado não pode ser habilitado', enableWarning: 'O arquivo arquivado não pode ser habilitado',
sync: 'Sincronizar', sync: 'Sincronizar',
resume: 'Retomar',
pause: 'Pausa',
}, },
index: { index: {
enable: 'Habilitar', enable: 'Habilitar',

@ -28,6 +28,8 @@ const translation = {
delete: 'Șterge', delete: 'Șterge',
enableWarning: 'Fișierul arhivat nu poate fi activat', enableWarning: 'Fișierul arhivat nu poate fi activat',
sync: 'Sincronizează', sync: 'Sincronizează',
pause: 'Pauză',
resume: 'Reia',
}, },
index: { index: {
enable: 'Activează', enable: 'Activează',

@ -29,6 +29,8 @@ const translation = {
delete: 'Удалить', delete: 'Удалить',
enableWarning: 'Архивный файл не может быть включен', enableWarning: 'Архивный файл не может быть включен',
sync: 'Синхронизировать', sync: 'Синхронизировать',
resume: 'Продовжити',
pause: 'Пауза',
}, },
index: { index: {
enable: 'Включить', enable: 'Включить',

@ -29,6 +29,8 @@ const translation = {
delete: 'Izbriši', delete: 'Izbriši',
enableWarning: 'Arhivirane datoteke ni mogoče omogočiti', enableWarning: 'Arhivirane datoteke ni mogoče omogočiti',
sync: 'Sinhroniziraj', sync: 'Sinhroniziraj',
pause: 'Zaustavi',
resume: 'Nadaljuj',
}, },
index: { index: {
enable: 'Omogoči', enable: 'Omogoči',
@ -332,7 +334,7 @@ const translation = {
previewTip: 'Predogled odstavkov bo na voljo po zaključku vdelave', previewTip: 'Predogled odstavkov bo na voljo po zaključku vdelave',
hierarchical: 'Starš-otrok', hierarchical: 'Starš-otrok',
childMaxTokens: 'Otrok', childMaxTokens: 'Otrok',
pause: 'Pavza', pause: 'Zaustavi',
parentMaxTokens: 'Starš', parentMaxTokens: 'Starš',
}, },
segment: { segment: {

@ -29,6 +29,8 @@ const translation = {
delete: 'ลบ', delete: 'ลบ',
enableWarning: 'ไม่สามารถเปิดใช้งานไฟล์ที่เก็บถาวรได้', enableWarning: 'ไม่สามารถเปิดใช้งานไฟล์ที่เก็บถาวรได้',
sync: 'ซิงค์', sync: 'ซิงค์',
pause: 'หยุด',
resume: 'ดำเนิน',
}, },
index: { index: {
enable: 'เปิด', enable: 'เปิด',

@ -29,6 +29,8 @@ const translation = {
delete: 'Sil', delete: 'Sil',
enableWarning: 'Arşivlenmiş dosya etkinleştirilemez', enableWarning: 'Arşivlenmiş dosya etkinleştirilemez',
sync: 'Senkronize et', sync: 'Senkronize et',
pause: 'Duraklat',
resume: 'Devam Et',
}, },
index: { index: {
enable: 'Etkinleştir', enable: 'Etkinleştir',

@ -28,6 +28,8 @@ const translation = {
delete: 'Видалити', delete: 'Видалити',
enableWarning: 'Архівований файл неможливо активувати', enableWarning: 'Архівований файл неможливо активувати',
sync: 'Синхронізувати', sync: 'Синхронізувати',
pause: 'Пауза',
resume: 'Продовжити',
}, },
index: { index: {
enable: 'Активувати', enable: 'Активувати',

@ -28,6 +28,8 @@ const translation = {
delete: 'Xóa', delete: 'Xóa',
enableWarning: 'Tệp đã lưu trữ không thể được kích hoạt', enableWarning: 'Tệp đã lưu trữ không thể được kích hoạt',
sync: 'Đồng bộ', sync: 'Đồng bộ',
pause: 'Tạm dừng',
resume: 'Tiếp tục',
}, },
index: { index: {
enable: 'Kích hoạt', enable: 'Kích hoạt',

@ -185,6 +185,10 @@ const translation = {
title: '编织', title: '编织',
description: 'Weave 是一个开源平台,用于评估、测试和监控大型语言模型应用程序。', description: 'Weave 是一个开源平台,用于评估、测试和监控大型语言模型应用程序。',
}, },
aliyun: {
title: '大模型可观测',
description: '阿里云提供的SaaS化可观测平台一键开启Dify应用的监控追踪和评估。',
},
}, },
appSelector: { appSelector: {
label: '应用', label: '应用',

@ -30,6 +30,8 @@ const translation = {
delete: '删除', delete: '删除',
enableWarning: '归档的文件无法启用', enableWarning: '归档的文件无法启用',
sync: '同步', sync: '同步',
pause: '暂停',
resume: '恢复',
}, },
index: { index: {
enable: '启用中', enable: '启用中',

@ -28,6 +28,8 @@ const translation = {
delete: '刪除', delete: '刪除',
enableWarning: '歸檔的檔案無法啟用', enableWarning: '歸檔的檔案無法啟用',
sync: '同步', sync: '同步',
resume: '恢復',
pause: '暫停',
}, },
index: { index: {
enable: '啟用中', enable: '啟用中',

@ -1,9 +1,9 @@
import type { ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' import type { AliyunConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
import type { App, AppTemplate, SiteConfig } from '@/types/app' import type { App, AppTemplate, SiteConfig } from '@/types/app'
import type { Dependency } from '@/app/components/plugins/types' import type { Dependency } from '@/app/components/plugins/types'
/* export type App = { /* export type App = {
id: string id: strin
name: string name: string
description: string description: string
mode: AppMode mode: AppMode
@ -166,5 +166,5 @@ export type TracingStatus = {
export type TracingConfig = { export type TracingConfig = {
tracing_provider: TracingProvider tracing_provider: TracingProvider
tracing_config: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig tracing_config: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig
} }

@ -5,6 +5,7 @@ import {
import { del, get, patch } from '../base' import { del, get, patch } from '../base'
import { useInvalid } from '../use-base' import { useInvalid } from '../use-base'
import type { MetadataType, SortType } from '../datasets' import type { MetadataType, SortType } from '../datasets'
import { pauseDocIndexing, resumeDocIndexing } from '../datasets'
import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets' import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets'
import { DocumentActionType } from '@/models/datasets' import { DocumentActionType } from '@/models/datasets'
import type { CommonResponse } from '@/models/common' import type { CommonResponse } from '@/models/common'
@ -130,3 +131,23 @@ export const useDocumentMetadata = (payload: {
export const useInvalidDocumentDetailKey = () => { export const useInvalidDocumentDetailKey = () => {
return useInvalid(useDocumentDetailKey) return useInvalid(useDocumentDetailKey)
} }
export const useDocumentPause = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
if (!datasetId || !documentId)
throw new Error('datasetId and documentId are required')
return pauseDocIndexing({ datasetId, documentId }) as Promise<CommonResponse>
},
})
}
export const useDocumentResume = () => {
return useMutation({
mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => {
if (!datasetId || !documentId)
throw new Error('datasetId and documentId are required')
return resumeDocIndexing({ datasetId, documentId }) as Promise<CommonResponse>
},
})
}

Loading…
Cancel
Save