Merge branch 'main' into mapped_column

pull/22644/head
Asuka Minato 9 months ago committed by GitHub
commit c87b29c383
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -29,7 +29,7 @@ from libs.login import login_required
from services.plugin.oauth_service import OAuthProxyService
from services.tools.api_tools_manage_service import ApiToolManageService
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
from services.tools.mcp_tools_mange_service import MCPToolManageService
from services.tools.mcp_tools_manage_service import MCPToolManageService
from services.tools.tool_labels_service import ToolLabelsService
from services.tools.tools_manage_service import ToolCommonService
from services.tools.tools_transform_service import ToolTransformService

@ -8,7 +8,7 @@ from core.mcp.types import (
OAuthTokens,
)
from models.tools import MCPToolProvider
from services.tools.mcp_tools_mange_service import MCPToolManageService
from services.tools.mcp_tools_manage_service import MCPToolManageService
LATEST_PROTOCOL_VERSION = "1.0"

@ -68,15 +68,17 @@ class MCPClient:
}
parsed_url = urlparse(self.server_url)
path = parsed_url.path
method_name = path.rstrip("/").split("/")[-1] if path else ""
try:
path = parsed_url.path or ""
method_name = path.removesuffix("/").lower()
if method_name in connection_methods:
client_factory = connection_methods[method_name]
self.connect_server(client_factory, method_name)
except KeyError:
else:
try:
logger.debug(f"Not supported method {method_name} found in URL path, trying default 'mcp' method.")
self.connect_server(sse_client, "sse")
except MCPConnectionError:
logger.debug("MCP connection failed with 'sse', falling back to 'mcp' method.")
self.connect_server(streamablehttp_client, "mcp")
def connect_server(
@ -91,7 +93,7 @@ class MCPClient:
else {}
)
self._streams_context = client_factory(url=self.server_url, headers=headers)
if self._streams_context is None:
if not self._streams_context:
raise MCPConnectionError("Failed to create connection context")
# Use exit_stack to manage context managers properly
@ -141,10 +143,11 @@ class MCPClient:
try:
# ExitStack will handle proper cleanup of all managed context managers
self.exit_stack.close()
except Exception as e:
logging.exception("Error during cleanup")
raise ValueError(f"Error during cleanup: {e}")
finally:
self._session = None
self._session_context = None
self._streams_context = None
self._initialized = False
except Exception as e:
logging.exception("Error during cleanup")
raise ValueError(f"Error during cleanup: {e}")

@ -233,6 +233,12 @@ class AnalyticdbVectorOpenAPI:
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
from alibabacloud_gpdb20160503 import models as gpdb_20160503_models
document_ids_filter = kwargs.get("document_ids_filter")
where_clause = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause += f"metadata_->>'document_id' IN ({document_ids})"
score_threshold = kwargs.get("score_threshold") or 0.0
request = gpdb_20160503_models.QueryCollectionDataRequest(
dbinstance_id=self.config.instance_id,
@ -245,7 +251,7 @@ class AnalyticdbVectorOpenAPI:
vector=query_vector,
content=None,
top_k=kwargs.get("top_k", 4),
filter=None,
filter=where_clause,
)
response = self._client.query_collection_data(request)
documents = []
@ -265,6 +271,11 @@ class AnalyticdbVectorOpenAPI:
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
from alibabacloud_gpdb20160503 import models as gpdb_20160503_models
document_ids_filter = kwargs.get("document_ids_filter")
where_clause = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause += f"metadata_->>'document_id' IN ({document_ids})"
score_threshold = float(kwargs.get("score_threshold") or 0.0)
request = gpdb_20160503_models.QueryCollectionDataRequest(
dbinstance_id=self.config.instance_id,
@ -277,7 +288,7 @@ class AnalyticdbVectorOpenAPI:
vector=None,
content=query,
top_k=kwargs.get("top_k", 4),
filter=None,
filter=where_clause,
)
response = self._client.query_collection_data(request)
documents = []

@ -147,10 +147,17 @@ class ElasticSearchVector(BaseVector):
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
query_str = {"match": {Field.CONTENT_KEY.value: query}}
query_str: dict[str, Any] = {"match": {Field.CONTENT_KEY.value: query}}
document_ids_filter = kwargs.get("document_ids_filter")
if document_ids_filter:
query_str["filter"] = {"terms": {"metadata.document_id": document_ids_filter}} # type: ignore
query_str = {
"bool": {
"must": {"match": {Field.CONTENT_KEY.value: query}},
"filter": {"terms": {"metadata.document_id": document_ids_filter}},
}
}
results = self._client.search(index=self._collection_name, query=query_str, size=kwargs.get("top_k", 4))
docs = []
for hit in results["hits"]["hits"]:

@ -21,7 +21,7 @@ from core.tools.plugin_tool.tool import PluginTool
from core.tools.utils.uuid_utils import is_valid_uuid
from core.tools.workflow_as_tool.provider import WorkflowToolProviderController
from core.workflow.entities.variable_pool import VariablePool
from services.tools.mcp_tools_mange_service import MCPToolManageService
from services.tools.mcp_tools_manage_service import MCPToolManageService
if TYPE_CHECKING:
from core.workflow.nodes.tool.entities import ToolEntity

@ -270,7 +270,14 @@ class AgentNode(BaseNode):
)
extra = tool.get("extra", {})
runtime_variable_pool = variable_pool if self._node_data.version != "1" else None
# This is an issue that caused problems before.
# Logically, we shouldn't use the node_data.version field for judgment
# But for backward compatibility with historical data
# this version field judgment is still preserved here.
runtime_variable_pool: VariablePool | None = None
if node_data.version != "1" or node_data.tool_node_version != "1":
runtime_variable_pool = variable_pool
tool_runtime = ToolManager.get_agent_tool_runtime(
self.tenant_id, self.app_id, entity, self.invoke_from, runtime_variable_pool
)

@ -13,6 +13,10 @@ class AgentNodeData(BaseNodeData):
agent_strategy_name: str
agent_strategy_label: str # redundancy
memory: MemoryConfig | None = None
# The version of the tool parameter.
# If this value is None, it indicates this is a previous version
# and requires using the legacy parameter parsing rules.
tool_node_version: str | None = None
class AgentInput(BaseModel):
value: Union[list[str], list[ToolSelector], Any]

@ -118,7 +118,7 @@ class KnowledgeRetrievalNodeData(BaseNodeData):
multiple_retrieval_config: Optional[MultipleRetrievalConfig] = None
single_retrieval_config: Optional[SingleRetrievalConfig] = None
metadata_filtering_mode: Optional[Literal["disabled", "automatic", "manual"]] = "disabled"
metadata_model_config: ModelConfig
metadata_model_config: Optional[ModelConfig] = None
metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None
vision: VisionConfig = Field(default_factory=VisionConfig)

@ -509,6 +509,8 @@ class KnowledgeRetrievalNode(BaseNode):
# get all metadata field
metadata_fields = db.session.query(DatasetMetadata).filter(DatasetMetadata.dataset_id.in_(dataset_ids)).all()
all_metadata_fields = [metadata_field.name for metadata_field in metadata_fields]
if node_data.metadata_model_config is None:
raise ValueError("metadata_model_config is required")
# get metadata model instance and fetch model config
model_instance, model_config = self.get_model_config(node_data.metadata_model_config)
# fetch prompt messages
@ -701,7 +703,7 @@ class KnowledgeRetrievalNode(BaseNode):
)
def _get_prompt_template(self, node_data: KnowledgeRetrievalNodeData, metadata_fields: list, query: str):
model_mode = ModelMode(node_data.metadata_model_config.mode)
model_mode = ModelMode(node_data.metadata_model_config.mode) # type: ignore
input_text = query
prompt_messages: list[LLMNodeChatModelMessage] = []

@ -73,6 +73,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = {
},
NodeType.TOOL: {
LATEST_VERSION: ToolNode,
# This is an issue that caused problems before.
# Logically, we shouldn't use two different versions to point to the same class here,
# but in order to maintain compatibility with historical data, this approach has been retained.
"2": ToolNode,
"1": ToolNode,
},
@ -123,6 +126,9 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = {
},
NodeType.AGENT: {
LATEST_VERSION: AgentNode,
# This is an issue that caused problems before.
# Logically, we shouldn't use two different versions to point to the same class here,
# but in order to maintain compatibility with historical data, this approach has been retained.
"2": AgentNode,
"1": AgentNode,
},

@ -59,6 +59,10 @@ class ToolNodeData(BaseNodeData, ToolEntity):
return typ
tool_parameters: dict[str, ToolInput]
# The version of the tool parameter.
# If this value is None, it indicates this is a previous version
# and requires using the legacy parameter parsing rules.
tool_node_version: str | None = None
@field_validator("tool_parameters", mode="before")
@classmethod

@ -70,7 +70,13 @@ class ToolNode(BaseNode):
try:
from core.tools.tool_manager import ToolManager
variable_pool = self.graph_runtime_state.variable_pool if self._node_data.version != "1" else None
# This is an issue that caused problems before.
# Logically, we shouldn't use the node_data.version field for judgment
# But for backward compatibility with historical data
# this version field judgment is still preserved here.
variable_pool: VariablePool | None = None
if node_data.version != "1" or node_data.tool_node_version != "1":
variable_pool = self.graph_runtime_state.variable_pool
tool_runtime = ToolManager.get_workflow_tool_runtime(
self.tenant_id, self.app_id, self.node_id, self._node_data, self.invoke_from, variable_pool
)

@ -0,0 +1,51 @@
"""update models
Revision ID: 1a83934ad6d1
Revises: 71f5020c6470
Create Date: 2025-07-21 09:35:48.774794
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1a83934ad6d1'
down_revision = '71f5020c6470'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_mcp_providers', schema=None) as batch_op:
batch_op.alter_column('server_identifier',
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=64),
existing_nullable=False)
with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op:
batch_op.alter_column('tool_name',
existing_type=sa.VARCHAR(length=40),
type_=sa.String(length=128),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_model_invokes', schema=None) as batch_op:
batch_op.alter_column('tool_name',
existing_type=sa.String(length=128),
type_=sa.VARCHAR(length=40),
existing_nullable=False)
with op.batch_alter_table('tool_mcp_providers', schema=None) as batch_op:
batch_op.alter_column('server_identifier',
existing_type=sa.String(length=64),
type_=sa.VARCHAR(length=24),
existing_nullable=False)
# ### end Alembic commands ###

@ -254,7 +254,7 @@ class MCPToolProvider(Base):
# name of the mcp provider
name: Mapped[str] = mapped_column(db.String(40), nullable=False)
# server identifier of the mcp provider
server_identifier: Mapped[str] = mapped_column(db.String(24), nullable=False)
server_identifier: Mapped[str] = mapped_column(db.String(64), nullable=False)
# encrypted url of the mcp provider
server_url: Mapped[str] = mapped_column(db.Text, nullable=False)
# hash of server_url for uniqueness check
@ -358,7 +358,7 @@ class ToolModelInvoke(Base):
# type
tool_type = mapped_column(db.String(40), nullable=False)
# tool name
tool_name = mapped_column(db.String(40), nullable=False)
tool_name = mapped_column(db.String(128), nullable=False)
# invoke parameters
model_parameters = mapped_column(db.Text, nullable=False)
# prompt messages

@ -1067,15 +1067,6 @@ class TenantService:
target_member_join.role = new_role
db.session.commit()
@staticmethod
def dissolve_tenant(tenant: Tenant, operator: Account) -> None:
"""Dissolve tenant"""
if not TenantService.check_member_permission(tenant, operator, operator, "remove"):
raise NoPermissionError("No permission to dissolve tenant.")
db.session.query(TenantAccountJoin).filter_by(tenant_id=tenant.id).delete()
db.session.delete(tenant)
db.session.commit()
@staticmethod
def get_custom_config(tenant_id: str) -> dict:
tenant = db.get_or_404(Tenant, tenant_id)

@ -70,16 +70,15 @@ class MCPToolManageService:
MCPToolProvider.server_url_hash == server_url_hash,
MCPToolProvider.server_identifier == server_identifier,
),
MCPToolProvider.tenant_id == tenant_id,
)
.first()
)
if existing_provider:
if existing_provider.name == name:
raise ValueError(f"MCP tool {name} already exists")
elif existing_provider.server_url_hash == server_url_hash:
if existing_provider.server_url_hash == server_url_hash:
raise ValueError(f"MCP tool {server_url} already exists")
elif existing_provider.server_identifier == server_identifier:
if existing_provider.server_identifier == server_identifier:
raise ValueError(f"MCP tool {server_identifier} already exists")
encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url)
mcp_tool = MCPToolProvider(
@ -111,15 +110,14 @@ class MCPToolManageService:
]
@classmethod
def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str):
def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str) -> ToolProviderApiEntity:
mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id)
try:
with MCPClient(
mcp_provider.decrypted_server_url, provider_id, tenant_id, authed=mcp_provider.authed, for_list=True
) as mcp_client:
tools = mcp_client.list_tools()
except MCPAuthError as e:
except MCPAuthError:
raise ValueError("Please auth the tool first")
except MCPError as e:
raise ValueError(f"Failed to connect to MCP server: {e}")
@ -184,12 +182,11 @@ class MCPToolManageService:
error_msg = str(e.orig)
if "unique_mcp_provider_name" in error_msg:
raise ValueError(f"MCP tool {name} already exists")
elif "unique_mcp_provider_server_url" in error_msg:
if "unique_mcp_provider_server_url" in error_msg:
raise ValueError(f"MCP tool {server_url} already exists")
elif "unique_mcp_provider_server_identifier" in error_msg:
if "unique_mcp_provider_server_identifier" in error_msg:
raise ValueError(f"MCP tool {server_identifier} already exists")
else:
raise
raise
@classmethod
def update_mcp_provider_credentials(

@ -0,0 +1,191 @@
from unittest.mock import MagicMock, patch
import pytest
import requests
from services.auth.firecrawl.firecrawl import FirecrawlAuth
class TestFirecrawlAuth:
@pytest.fixture
def valid_credentials(self):
"""Fixture for valid bearer credentials"""
return {"auth_type": "bearer", "config": {"api_key": "test_api_key_123"}}
@pytest.fixture
def auth_instance(self, valid_credentials):
"""Fixture for FirecrawlAuth instance with valid credentials"""
return FirecrawlAuth(valid_credentials)
def test_should_initialize_with_valid_bearer_credentials(self, valid_credentials):
"""Test successful initialization with valid bearer credentials"""
auth = FirecrawlAuth(valid_credentials)
assert auth.api_key == "test_api_key_123"
assert auth.base_url == "https://api.firecrawl.dev"
assert auth.credentials == valid_credentials
def test_should_initialize_with_custom_base_url(self):
"""Test initialization with custom base URL"""
credentials = {
"auth_type": "bearer",
"config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
}
auth = FirecrawlAuth(credentials)
assert auth.api_key == "test_api_key_123"
assert auth.base_url == "https://custom.firecrawl.dev"
@pytest.mark.parametrize(
("auth_type", "expected_error"),
[
("basic", "Invalid auth type, Firecrawl auth type must be Bearer"),
("x-api-key", "Invalid auth type, Firecrawl auth type must be Bearer"),
("", "Invalid auth type, Firecrawl auth type must be Bearer"),
],
)
def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error):
"""Test that non-bearer auth types raise ValueError"""
credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}}
with pytest.raises(ValueError) as exc_info:
FirecrawlAuth(credentials)
assert str(exc_info.value) == expected_error
@pytest.mark.parametrize(
("credentials", "expected_error"),
[
({"auth_type": "bearer", "config": {}}, "No API key provided"),
({"auth_type": "bearer"}, "No API key provided"),
({"auth_type": "bearer", "config": {"api_key": ""}}, "No API key provided"),
({"auth_type": "bearer", "config": {"api_key": None}}, "No API key provided"),
],
)
def test_should_raise_error_for_missing_api_key(self, credentials, expected_error):
"""Test that missing or empty API key raises ValueError"""
with pytest.raises(ValueError) as exc_info:
FirecrawlAuth(credentials)
assert str(exc_info.value) == expected_error
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_validate_valid_credentials_successfully(self, mock_post, auth_instance):
"""Test successful credential validation"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_post.return_value = mock_response
result = auth_instance.validate_credentials()
assert result is True
expected_data = {
"url": "https://example.com",
"includePaths": [],
"excludePaths": [],
"limit": 1,
"scrapeOptions": {"onlyMainContent": True},
}
mock_post.assert_called_once_with(
"https://api.firecrawl.dev/v1/crawl",
headers={"Content-Type": "application/json", "Authorization": "Bearer test_api_key_123"},
json=expected_data,
)
@pytest.mark.parametrize(
("status_code", "error_message"),
[
(402, "Payment required"),
(409, "Conflict error"),
(500, "Internal server error"),
],
)
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_handle_http_errors(self, mock_post, status_code, error_message, auth_instance):
"""Test handling of various HTTP error codes"""
mock_response = MagicMock()
mock_response.status_code = status_code
mock_response.json.return_value = {"error": error_message}
mock_post.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
@pytest.mark.parametrize(
("status_code", "response_text", "has_json_error", "expected_error_contains"),
[
(403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
(404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
(401, "Not JSON", True, "Expecting value"), # JSON decode error
],
)
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_handle_unexpected_errors(
self, mock_post, status_code, response_text, has_json_error, expected_error_contains, auth_instance
):
"""Test handling of unexpected errors with various response formats"""
mock_response = MagicMock()
mock_response.status_code = status_code
mock_response.text = response_text
if has_json_error:
mock_response.json.side_effect = Exception("Not JSON")
mock_post.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert expected_error_contains in str(exc_info.value)
@pytest.mark.parametrize(
("exception_type", "exception_message"),
[
(requests.ConnectionError, "Network error"),
(requests.Timeout, "Request timeout"),
(requests.ReadTimeout, "Read timeout"),
(requests.ConnectTimeout, "Connection timeout"),
],
)
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_handle_network_errors(self, mock_post, exception_type, exception_message, auth_instance):
"""Test handling of various network-related errors including timeouts"""
mock_post.side_effect = exception_type(exception_message)
with pytest.raises(exception_type) as exc_info:
auth_instance.validate_credentials()
assert exception_message in str(exc_info.value)
def test_should_not_expose_api_key_in_error_messages(self):
"""Test that API key is not exposed in error messages"""
credentials = {"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}}
auth = FirecrawlAuth(credentials)
# Verify API key is stored but not in any error message
assert auth.api_key == "super_secret_key_12345"
# Test various error scenarios don't expose the key
with pytest.raises(ValueError) as exc_info:
FirecrawlAuth({"auth_type": "basic", "config": {"api_key": "super_secret_key_12345"}})
assert "super_secret_key_12345" not in str(exc_info.value)
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_use_custom_base_url_in_validation(self, mock_post):
"""Test that custom base URL is used in validation"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_post.return_value = mock_response
credentials = {
"auth_type": "bearer",
"config": {"api_key": "test_api_key_123", "base_url": "https://custom.firecrawl.dev"},
}
auth = FirecrawlAuth(credentials)
result = auth.validate_credentials()
assert result is True
assert mock_post.call_args[0][0] == "https://custom.firecrawl.dev/v1/crawl"
@patch("services.auth.firecrawl.firecrawl.requests.post")
def test_should_handle_timeout_with_retry_suggestion(self, mock_post, auth_instance):
"""Test that timeout errors are handled gracefully with appropriate error message"""
mock_post.side_effect = requests.Timeout("The request timed out after 30 seconds")
with pytest.raises(requests.Timeout) as exc_info:
auth_instance.validate_credentials()
# Verify the timeout exception is raised with original message
assert "timed out" in str(exc_info.value)

@ -0,0 +1,205 @@
from unittest.mock import MagicMock, patch
import pytest
import requests
from services.auth.watercrawl.watercrawl import WatercrawlAuth
class TestWatercrawlAuth:
@pytest.fixture
def valid_credentials(self):
"""Fixture for valid x-api-key credentials"""
return {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123"}}
@pytest.fixture
def auth_instance(self, valid_credentials):
"""Fixture for WatercrawlAuth instance with valid credentials"""
return WatercrawlAuth(valid_credentials)
def test_should_initialize_with_valid_x_api_key_credentials(self, valid_credentials):
"""Test successful initialization with valid x-api-key credentials"""
auth = WatercrawlAuth(valid_credentials)
assert auth.api_key == "test_api_key_123"
assert auth.base_url == "https://app.watercrawl.dev"
assert auth.credentials == valid_credentials
def test_should_initialize_with_custom_base_url(self):
"""Test initialization with custom base URL"""
credentials = {
"auth_type": "x-api-key",
"config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"},
}
auth = WatercrawlAuth(credentials)
assert auth.api_key == "test_api_key_123"
assert auth.base_url == "https://custom.watercrawl.dev"
@pytest.mark.parametrize(
("auth_type", "expected_error"),
[
("bearer", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
("basic", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
("", "Invalid auth type, WaterCrawl auth type must be x-api-key"),
],
)
def test_should_raise_error_for_invalid_auth_type(self, auth_type, expected_error):
"""Test that non-x-api-key auth types raise ValueError"""
credentials = {"auth_type": auth_type, "config": {"api_key": "test_api_key_123"}}
with pytest.raises(ValueError) as exc_info:
WatercrawlAuth(credentials)
assert str(exc_info.value) == expected_error
@pytest.mark.parametrize(
("credentials", "expected_error"),
[
({"auth_type": "x-api-key", "config": {}}, "No API key provided"),
({"auth_type": "x-api-key"}, "No API key provided"),
({"auth_type": "x-api-key", "config": {"api_key": ""}}, "No API key provided"),
({"auth_type": "x-api-key", "config": {"api_key": None}}, "No API key provided"),
],
)
def test_should_raise_error_for_missing_api_key(self, credentials, expected_error):
"""Test that missing or empty API key raises ValueError"""
with pytest.raises(ValueError) as exc_info:
WatercrawlAuth(credentials)
assert str(exc_info.value) == expected_error
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_validate_valid_credentials_successfully(self, mock_get, auth_instance):
"""Test successful credential validation"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_get.return_value = mock_response
result = auth_instance.validate_credentials()
assert result is True
mock_get.assert_called_once_with(
"https://app.watercrawl.dev/api/v1/core/crawl-requests/",
headers={"Content-Type": "application/json", "X-API-KEY": "test_api_key_123"},
)
@pytest.mark.parametrize(
("status_code", "error_message"),
[
(402, "Payment required"),
(409, "Conflict error"),
(500, "Internal server error"),
],
)
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_handle_http_errors(self, mock_get, status_code, error_message, auth_instance):
"""Test handling of various HTTP error codes"""
mock_response = MagicMock()
mock_response.status_code = status_code
mock_response.json.return_value = {"error": error_message}
mock_get.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert str(exc_info.value) == f"Failed to authorize. Status code: {status_code}. Error: {error_message}"
@pytest.mark.parametrize(
("status_code", "response_text", "has_json_error", "expected_error_contains"),
[
(403, '{"error": "Forbidden"}', True, "Failed to authorize. Status code: 403. Error: Forbidden"),
(404, "", True, "Unexpected error occurred while trying to authorize. Status code: 404"),
(401, "Not JSON", True, "Expecting value"), # JSON decode error
],
)
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_handle_unexpected_errors(
self, mock_get, status_code, response_text, has_json_error, expected_error_contains, auth_instance
):
"""Test handling of unexpected errors with various response formats"""
mock_response = MagicMock()
mock_response.status_code = status_code
mock_response.text = response_text
if has_json_error:
mock_response.json.side_effect = Exception("Not JSON")
mock_get.return_value = mock_response
with pytest.raises(Exception) as exc_info:
auth_instance.validate_credentials()
assert expected_error_contains in str(exc_info.value)
@pytest.mark.parametrize(
("exception_type", "exception_message"),
[
(requests.ConnectionError, "Network error"),
(requests.Timeout, "Request timeout"),
(requests.ReadTimeout, "Read timeout"),
(requests.ConnectTimeout, "Connection timeout"),
],
)
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_handle_network_errors(self, mock_get, exception_type, exception_message, auth_instance):
"""Test handling of various network-related errors including timeouts"""
mock_get.side_effect = exception_type(exception_message)
with pytest.raises(exception_type) as exc_info:
auth_instance.validate_credentials()
assert exception_message in str(exc_info.value)
def test_should_not_expose_api_key_in_error_messages(self):
"""Test that API key is not exposed in error messages"""
credentials = {"auth_type": "x-api-key", "config": {"api_key": "super_secret_key_12345"}}
auth = WatercrawlAuth(credentials)
# Verify API key is stored but not in any error message
assert auth.api_key == "super_secret_key_12345"
# Test various error scenarios don't expose the key
with pytest.raises(ValueError) as exc_info:
WatercrawlAuth({"auth_type": "bearer", "config": {"api_key": "super_secret_key_12345"}})
assert "super_secret_key_12345" not in str(exc_info.value)
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_use_custom_base_url_in_validation(self, mock_get):
"""Test that custom base URL is used in validation"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_get.return_value = mock_response
credentials = {
"auth_type": "x-api-key",
"config": {"api_key": "test_api_key_123", "base_url": "https://custom.watercrawl.dev"},
}
auth = WatercrawlAuth(credentials)
result = auth.validate_credentials()
assert result is True
assert mock_get.call_args[0][0] == "https://custom.watercrawl.dev/api/v1/core/crawl-requests/"
@pytest.mark.parametrize(
("base_url", "expected_url"),
[
("https://app.watercrawl.dev", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
("https://app.watercrawl.dev/", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
("https://app.watercrawl.dev//", "https://app.watercrawl.dev/api/v1/core/crawl-requests/"),
],
)
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_use_urljoin_for_url_construction(self, mock_get, base_url, expected_url):
"""Test that urljoin is used correctly for URL construction with various base URLs"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_get.return_value = mock_response
credentials = {"auth_type": "x-api-key", "config": {"api_key": "test_api_key_123", "base_url": base_url}}
auth = WatercrawlAuth(credentials)
auth.validate_credentials()
# Verify the correct URL was called
assert mock_get.call_args[0][0] == expected_url
@patch("services.auth.watercrawl.watercrawl.requests.get")
def test_should_handle_timeout_with_retry_suggestion(self, mock_get, auth_instance):
"""Test that timeout errors are handled gracefully with appropriate error message"""
mock_get.side_effect = requests.Timeout("The request timed out after 30 seconds")
with pytest.raises(requests.Timeout) as exc_info:
auth_instance.validate_credentials()
# Verify the timeout exception is raised with original message
assert "timed out" in str(exc_info.value)

@ -283,11 +283,12 @@ REDIS_CLUSTERS_PASSWORD=
# Celery Configuration
# ------------------------------
# Use redis as the broker, and redis db 1 for celery broker.
# Format as follows: `redis://<redis_username>:<redis_password>@<redis_host>:<redis_port>/<redis_database>`
# Use standalone redis as the broker, and redis db 1 for celery broker. (redis_username is usually set by defualt as empty)
# Format as follows: `redis://<redis_username>:<redis_password>@<redis_host>:<redis_port>/<redis_database>`.
# Example: redis://:difyai123456@redis:6379/1
# If use Redis Sentinel, format as follows: `sentinel://<sentinel_username>:<sentinel_password>@<sentinel_host>:<sentinel_port>/<redis_database>`
# Example: sentinel://localhost:26379/1;sentinel://localhost:26380/1;sentinel://localhost:26381/1
# If use Redis Sentinel, format as follows: `sentinel://<redis_username>:<redis_password>@<sentinel_host1>:<sentinel_port>/<redis_database>`
# For high availability, you can configure multiple Sentinel nodes (if provided) separated by semicolons like below example:
# Example: sentinel://:difyai123456@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1;sentinel://:difyai12345@localhost:26379/1
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
CELERY_BACKEND=redis
BROKER_USE_SSL=false

@ -83,7 +83,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション)
- <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property>
<PropertyInstruction>ナレッジベースにパラメータが設定されていない場合、最初のアップロードには以下のパラメータを提供する必要があります。提供されない場合、デフォルトパラメータが使用されます。</PropertyInstruction>
<Property name='retrieval_model' type='object' key='retrieval_model'>
@ -218,7 +218,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション)
- <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property>
<Property name='file' type='multipart/form-data' key='file'>
アップロードする必要があるファイル。
@ -555,7 +555,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション)
- <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property>
</Properties>
</Col>
@ -657,7 +657,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>subchunk_segmentation</code> (object) 子チャンクルール
- <code>separator</code> セグメンテーション識別子。現在は 1 つの区切り文字のみ許可。デフォルトは <code>***</code>
- <code>max_tokens</code> 最大長 (トークン) は親チャンクの長さより短いことを検証する必要があります
- <code>chunk_overlap</code> 隣接するチャンク間の重を定義 (オプション)
- <code>chunk_overlap</code> 隣接するチャンク間の重なりを定義 (オプション)
</Property>
</Properties>
</Col>

@ -1,6 +1,6 @@
import React from 'react'
import type { ReactNode } from 'react'
import SwrInitor from '@/app/components/swr-initor'
import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
@ -13,7 +13,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<GA gaType={GaType.admin} />
<SwrInitor>
<SwrInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -26,7 +26,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</SwrInitor>
</SwrInitializer>
</>
)
}

@ -1,5 +1,6 @@
'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiGraduationCapFill,
@ -22,6 +23,8 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
import { fetchAppList } from '@/service/apps'
import type { App } from '@/types/app'
const titleClassName = `
system-sm-semibold text-text-secondary
@ -33,7 +36,9 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext()
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
const apps = appList?.data || []
const { mutateUserProfile, userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
@ -202,7 +207,7 @@ export default function AccountPage() {
{!!apps.length && (
<Collapse
title={`${t('common.account.showAppLength', { length: apps.length })}`}
items={apps.map(app => ({ ...app, key: app.id, name: app.name }))}
items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
renderItem={renderAppItem}
wrapperClassName='mt-2'
/>

@ -1,7 +1,7 @@
import React from 'react'
import type { ReactNode } from 'react'
import Header from './header'
import SwrInitor from '@/app/components/swr-initor'
import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'

@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import React, { useCallback, useState } from 'react'
import {
RiDeleteBinLine,
@ -15,7 +15,7 @@ import AppIcon from '../base/app-icon'
import cn from '@/utils/classnames'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import AppsContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
@ -73,11 +73,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon_type,
@ -106,12 +101,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
message: t('app.editDone'),
})
setAppDetail(app)
mutateApps()
}
catch {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [appDetail, mutateApps, notify, setAppDetail, t])
}, [appDetail, notify, setAppDetail, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
if (!appDetail)
@ -131,7 +125,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
mutateApps()
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
@ -186,7 +179,6 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
mutateApps()
onPlanInfoChanged()
setAppDetail()
replace('/apps')
@ -198,7 +190,7 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
})
}
setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
}, [appDetail, notify, onPlanInfoChanged, replace, setAppDetail, t])
const { isCurrentWorkspaceEditor } = useAppContext()

@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
import Link from 'next/link'
import { useDebounceFn, useKeyPress } from 'ahooks'
@ -15,7 +15,7 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
import AppsContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import type { AppMode } from '@/types/app'
@ -41,7 +41,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
const { t } = useTranslation()
const { push } = useRouter()
const { notify } = useContext(ToastContext)
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const [appMode, setAppMode] = useState<AppMode>('advanced-chat')
const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
@ -80,7 +79,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
notify({ type: 'success', message: t('app.newApp.appCreated') })
onSuccess()
onClose()
mutateApps()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, app, push)
}
@ -88,7 +86,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor])
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
@ -298,7 +296,7 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
>
{icon}
<div className='system-sm-semibold mb-0.5 mt-2 text-text-secondary'>{title}</div>
<div className='system-xs-regular text-text-tertiary'>{description}</div>
<div className='system-xs-regular line-clamp-2 text-text-tertiary' title={description}>{description}</div>
</div>
}

@ -90,10 +90,10 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
const [option, setOption] = useState<Option>('iframe')
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
const { langeniusVersionInfo } = useAppContext()
const { langGeniusVersionInfo } = useAppContext()
const themeBuilder = useThemeContext()
themeBuilder.buildTheme(siteInfo?.chat_color_theme ?? null, siteInfo?.chat_color_theme_inverted ?? false)
const isTestEnv = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
const isTestEnv = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const onClickCopy = () => {
if (option === 'chromePlugin') {
const splitUrl = OPTION_MAP[option].getContent(appBaseUrl, accessToken).split(': ')

@ -1,7 +1,7 @@
'use client'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
@ -11,7 +11,7 @@ import Toast, { ToastContext } from '@/app/components/base/toast'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
@ -65,11 +65,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { onPlanInfoChanged } = useProviderContext()
const { push } = useRouter()
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
@ -83,7 +78,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
notify({ type: 'success', message: t('app.appDeleted') })
if (onRefresh)
onRefresh()
mutateApps()
onPlanInfoChanged()
}
catch (e: any) {
@ -93,7 +87,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
})
}
setShowConfirmDelete(false)
}, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t])
}, [app.id, notify, onPlanInfoChanged, onRefresh, t])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
@ -122,12 +116,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
})
if (onRefresh)
onRefresh()
mutateApps()
}
catch {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [app.id, mutateApps, notify, onRefresh, t])
}, [app.id, notify, onRefresh, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
try {
@ -147,7 +140,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (onRefresh)
onRefresh()
mutateApps()
onPlanInfoChanged()
getRedirection(isCurrentWorkspaceEditor, newApp, push)
}
@ -195,16 +187,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
const onSwitch = () => {
if (onRefresh)
onRefresh()
mutateApps()
setShowSwitchModal(false)
}
const onUpdateAccessControl = useCallback(() => {
if (onRefresh)
onRefresh()
mutateApps()
setShowAccessControl(false)
}, [onRefresh, mutateApps, setShowAccessControl])
}, [onRefresh, setShowAccessControl])
const Operations = (props: HtmlContentProps) => {
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: app?.id, enabled: (!!props?.open && systemFeatures.webapp_auth.enabled) })
@ -325,7 +315,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
dateFormat: `${t('datasetDocuments.segment.dateTimeFormat')}`,
})
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app.updated_at, app.created_at])
return (

@ -45,7 +45,7 @@ const Avatar = ({
className={cn(textClassName, 'scale-[0.4] text-center text-white')}
style={style}
>
{name[0].toLocaleUpperCase()}
{name && name[0].toLocaleUpperCase()}
</div>
</div>
)

@ -117,7 +117,7 @@ const Question: FC<QuestionProps> = ({
</div>
<div
ref={contentRef}
className='bg-background-gradient-bg-fill-chat-bubble-bg-3 w-full rounded-2xl px-4 py-3 text-sm text-text-primary'
className='w-full rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 px-4 py-3 text-sm text-text-primary'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
>
{

@ -21,7 +21,7 @@ const AppsFull: FC<{ loc: string; className?: string; }> = ({
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { userProfile, langeniusVersionInfo } = useAppContext()
const { userProfile, langGeniusVersionInfo } = useAppContext()
const isTeam = plan.type === Plan.team
const usage = plan.usage.buildApps
const total = plan.total.buildApps
@ -62,7 +62,7 @@ const AppsFull: FC<{ loc: string; className?: string; }> = ({
)}
{plan.type !== Plan.sandbox && plan.type !== Plan.professional && (
<Button variant='secondary-accent'>
<a target='_blank' rel='noopener noreferrer' href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}>
<a target='_blank' rel='noopener noreferrer' href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo.current_version)}>
{t('billing.apps.contactUs')}
</a>
</Button>

@ -43,10 +43,10 @@ Object.defineProperty(globalThis, 'sessionStorage', {
value: sessionStorage,
})
const BrowserInitor = ({
const BrowserInitializer = ({
children,
}: { children: React.ReactNode }) => {
}: { children: React.ReactElement }) => {
return children
}
export default BrowserInitor
export default BrowserInitializer

@ -12,16 +12,16 @@ import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
type IAccountSettingProps = {
langeniusVersionInfo: LangGeniusVersionResponse
langGeniusVersionInfo: LangGeniusVersionResponse
onCancel: () => void
}
export default function AccountAbout({
langeniusVersionInfo,
langGeniusVersionInfo,
onCancel,
}: IAccountSettingProps) {
const { t } = useTranslation()
const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version
const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return (
@ -43,7 +43,7 @@ export default function AccountAbout({
/>
: <DifyLogo size='large' className='mx-auto' />}
<div className='text-center text-xs font-normal text-text-tertiary'>Version {langeniusVersionInfo?.current_version}</div>
<div className='text-center text-xs font-normal text-text-tertiary'>Version {langGeniusVersionInfo?.current_version}</div>
<div className='flex flex-col items-center gap-2 text-center text-xs font-normal text-text-secondary'>
<div>© {dayjs().year()} LangGenius, Inc., Contributors.</div>
<div className='text-text-accent'>
@ -63,8 +63,8 @@ export default function AccountAbout({
<div className='text-xs font-medium text-text-tertiary'>
{
isLatest
? t('common.about.latestAvailable', { version: langeniusVersionInfo.latest_version })
: t('common.about.nowAvailable', { version: langeniusVersionInfo.latest_version })
? t('common.about.latestAvailable', { version: langGeniusVersionInfo.latest_version })
: t('common.about.nowAvailable', { version: langGeniusVersionInfo.latest_version })
}
</div>
<div className='flex items-center'>
@ -80,7 +80,7 @@ export default function AccountAbout({
!isLatest && !IS_CE_EDITION && (
<Button variant='primary' size='small'>
<Link
href={langeniusVersionInfo.release_notes}
href={langGeniusVersionInfo.release_notes}
target='_blank' rel='noopener noreferrer'
>
{t('common.about.updateNow')}

@ -45,7 +45,7 @@ export default function AppSelector() {
const { t } = useTranslation()
const docLink = useDocLink()
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const { userProfile, langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
@ -180,8 +180,8 @@ export default function AppSelector() {
<RiInformation2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.about')}</div>
<div className='flex shrink-0 items-center'>
<div className='system-xs-regular mr-2 text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
<div className='system-xs-regular mr-2 text-text-tertiary'>{langGeniusVersionInfo.current_version}</div>
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
</div>
</MenuItem>
@ -217,7 +217,7 @@ export default function AppSelector() {
}
</Menu>
{
aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langeniusVersionInfo={langeniusVersionInfo} />
aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />
}
</div >
)

@ -16,7 +16,7 @@ export default function Support() {
`
const { t } = useTranslation()
const { plan } = useProviderContext()
const { userProfile, langeniusVersionInfo } = useAppContext()
const { userProfile, langGeniusVersionInfo } = useAppContext()
const canEmailSupport = plan.type === Plan.professional || plan.type === Plan.team || plan.type === Plan.enterprise
return <Menu as="div" className="relative h-full w-full">
@ -53,7 +53,7 @@ export default function Support() {
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo.current_version)}
target='_blank' rel='noopener noreferrer'>
<RiMailSendLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.emailSupport')}</div>

@ -12,8 +12,8 @@ const headerEnvClassName: { [k: string]: string } = {
const EnvNav = () => {
const { t } = useTranslation()
const { langeniusVersionInfo } = useAppContext()
const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT'
const { langGeniusVersionInfo } = useAppContext()
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
if (!showEnvTag)
return null
@ -21,10 +21,10 @@ const EnvNav = () => {
return (
<div className={`
mr-1 flex h-[22px] items-center rounded-md border px-2 text-xs font-medium
${headerEnvClassName[langeniusVersionInfo.current_env]}
${headerEnvClassName[langGeniusVersionInfo.current_env]}
`}>
{
langeniusVersionInfo.current_env === 'TESTING' && (
langGeniusVersionInfo.current_env === 'TESTING' && (
<>
<Beaker02 className='h-3 w-3' />
<div className='ml-1 max-[1280px]:hidden'>{t('common.environment.testing')}</div>
@ -32,7 +32,7 @@ const EnvNav = () => {
)
}
{
langeniusVersionInfo.current_env === 'DEVELOPMENT' && (
langGeniusVersionInfo.current_env === 'DEVELOPMENT' && (
<>
<TerminalSquare className='h-3 w-3' />
<div className='ml-1 max-[1280px]:hidden'>{t('common.environment.development')}</div>

@ -48,7 +48,6 @@ const Installed: FC<Props> = ({
useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
onInstalled()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasInstalled])
const [isInstalling, setIsInstalling] = React.useState(false)
@ -105,12 +104,12 @@ const Installed: FC<Props> = ({
}
}
const { langeniusVersionInfo } = useAppContext()
const { langGeniusVersionInfo } = useAppContext()
const isDifyVersionCompatible = useMemo(() => {
if (!langeniusVersionInfo.current_version)
if (!langGeniusVersionInfo.current_version)
return true
return gte(langeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0')
}, [langeniusVersionInfo.current_version, payload.meta.minimum_dify_version])
return gte(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0')
}, [langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version])
return (
<>

@ -59,7 +59,6 @@ const Installed: FC<Props> = ({
useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
onInstalled()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasInstalled])
const handleCancel = () => {
@ -120,12 +119,12 @@ const Installed: FC<Props> = ({
}
}
const { langeniusVersionInfo } = useAppContext()
const { langGeniusVersionInfo } = useAppContext()
const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier)
const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true
return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langeniusVersionInfo.current_version, pluginDeclaration])
if (!pluginDeclaration || !langGeniusVersionInfo.current_version) return true
return gte(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langGeniusVersionInfo.current_version, pluginDeclaration])
const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
return (

@ -265,7 +265,7 @@ const ToolSelector: FC<Props> = ({
/>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<PortalToFollowElemContent className='z-10'>
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
<>
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
@ -309,15 +309,15 @@ const ToolSelector: FC<Props> = ({
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
<div className='px-4 py-2'>
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
</div>
</>
)}

@ -62,13 +62,13 @@ const PluginItem: FC<Props> = ({
return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : ''
}, [source, author])
const { langeniusVersionInfo } = useAppContext()
const { langGeniusVersionInfo } = useAppContext()
const isDifyVersionCompatible = useMemo(() => {
if (!langeniusVersionInfo.current_version)
if (!langGeniusVersionInfo.current_version)
return true
return gte(langeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0')
}, [declarationMeta.minimum_dify_version, langeniusVersionInfo.current_version])
return gte(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0')
}, [declarationMeta.minimum_dify_version, langGeniusVersionInfo.current_version])
const handleDelete = () => {
refreshPluginList({ category } as any)

@ -5,9 +5,9 @@ import * as Sentry from '@sentry/react'
const isDevelopment = process.env.NODE_ENV === 'development'
const SentryInit = ({
const SentryInitializer = ({
children,
}: { children: React.ReactNode }) => {
}: { children: React.ReactElement }) => {
useEffect(() => {
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
if (!isDevelopment && SENTRY_DSN) {
@ -26,4 +26,4 @@ const SentryInit = ({
return children
}
export default SentryInit
export default SentryInitializer

@ -10,12 +10,12 @@ import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
} from '@/app/education-apply/constants'
type SwrInitorProps = {
type SwrInitializerProps = {
children: ReactNode
}
const SwrInitor = ({
const SwrInitializer = ({
children,
}: SwrInitorProps) => {
}: SwrInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
@ -86,4 +86,4 @@ const SwrInitor = ({
: null
}
export default SwrInitor
export default SwrInitializer

@ -7,7 +7,7 @@ import { renderI18nObject } from '@/i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
version: '2',
tool_node_version: '2',
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
@ -62,27 +62,29 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
const version = payload.version
const toolNodeVersion = payload.tool_node_version
const mergeVersion = version || toolNodeVersion
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && !mergeVersion && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) {
if (schema.form === 'form' && mergeVersion && !userSettings[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
if (schema.form === 'llm' && !mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
if (schema.form === 'llm' && mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),

@ -12,6 +12,7 @@ export type AgentNodeType = CommonNodeType & {
plugin_unique_identifier?: string
memory?: Memory
version?: string
tool_node_version?: string
}
export enum AgentFeature {

@ -129,7 +129,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
}
const formattingLegacyData = () => {
if (inputs.version)
if (inputs.version || inputs.tool_node_version)
return inputs
const newData = produce(inputs, (draft) => {
const schemas = currentStrategy?.parameters || []
@ -140,7 +140,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
if (targetSchema?.type === FormTypeEnum.multiToolSelector)
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool))
})
draft.version = '2'
draft.tool_node_version = '2'
})
return newData
}

@ -10,7 +10,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = {
defaultValue: {
tool_parameters: {},
tool_configurations: {},
version: '2',
tool_node_version: '2',
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode

@ -23,4 +23,5 @@ export type ToolNodeType = CommonNodeType & {
output_schema: Record<string, any>
paramSchemas?: Record<string, any>[]
version?: string
tool_node_version?: string
}

@ -286,8 +286,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
}
}
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version) {
(node as Node<ToolNodeType>).data.version = '2'
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version && !(node as Node<ToolNodeType>).data.tool_node_version) {
(node as Node<ToolNodeType>).data.tool_node_version = '2'
const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations
if (toolConfigurations && Object.keys(toolConfigurations).length > 0) {

@ -1,10 +1,10 @@
import RoutePrefixHandle from './routePrefixHandle'
import type { Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor'
import BrowserInitializer from './components/browser-initializer'
import SentryInitializer from './components/sentry-initializer'
import { getLocaleOnServer } from '@/i18n/server'
import { TanstackQueryIniter } from '@/context/query-client'
import { TanstackQueryInitializer } from '@/context/query-client'
import { ThemeProvider } from 'next-themes'
import './styles/globals.css'
import './styles/markdown.scss'
@ -62,9 +62,9 @@ const LocaleLayout = async ({
className="color-scheme h-full select-auto"
{...datasetMap}
>
<BrowserInitor>
<SentryInitor>
<TanstackQueryIniter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<ThemeProvider
attribute='data-theme'
defaultTheme='system'
@ -77,9 +77,9 @@ const LocaleLayout = async ({
</GlobalPublicStoreProvider>
</I18nServer>
</ThemeProvider>
</TanstackQueryIniter>
</SentryInitor>
</BrowserInitor>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
<RoutePrefixHandle />
</body>
</html>

@ -1,20 +1,15 @@
'use client'
import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
import type { App } from '@/types/app'
import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { noop } from 'lodash-es'
export type AppContextValue = {
apps: App[]
mutateApps: VoidFunction
userProfile: UserProfileResponse
mutateUserProfile: VoidFunction
currentWorkspace: ICurrentWorkspace
@ -23,13 +18,21 @@ export type AppContextValue = {
isCurrentWorkspaceEditor: boolean
isCurrentWorkspaceDatasetOperator: boolean
mutateCurrentWorkspace: VoidFunction
pageContainerRef: React.RefObject<HTMLDivElement>
langeniusVersionInfo: LangGeniusVersionResponse
langGeniusVersionInfo: LangGeniusVersionResponse
useSelector: typeof useSelector
isLoadingCurrentWorkspace: boolean
}
const initialLangeniusVersionInfo = {
const userProfilePlaceholder = {
id: '',
name: '',
email: '',
avatar: '',
avatar_url: '',
is_password_set: false,
}
const initialLangGeniusVersionInfo = {
current_env: '',
current_version: '',
latest_version: '',
@ -50,16 +53,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
}
const AppContext = createContext<AppContextValue>({
apps: [],
mutateApps: noop,
userProfile: {
id: '',
name: '',
email: '',
avatar: '',
avatar_url: '',
is_password_set: false,
},
userProfile: userProfilePlaceholder,
currentWorkspace: initialWorkspaceInfo,
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
@ -67,8 +61,7 @@ const AppContext = createContext<AppContextValue>({
isCurrentWorkspaceDatasetOperator: false,
mutateUserProfile: noop,
mutateCurrentWorkspace: noop,
pageContainerRef: createRef(),
langeniusVersionInfo: initialLangeniusVersionInfo,
langGeniusVersionInfo: initialLangGeniusVersionInfo,
useSelector,
isLoadingCurrentWorkspace: false,
})
@ -82,14 +75,11 @@ export type AppContextProviderProps = {
}
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
const pageContainerRef = useRef<HTMLDivElement>(null)
const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1, limit: 30, name: '' } }, fetchAppList)
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
const [userProfile, setUserProfile] = useState<UserProfileResponse>(userProfilePlaceholder)
const [langGeniusVersionInfo, setLangGeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangGeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
@ -101,8 +91,8 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
setUserProfile(result)
const current_version = userProfileResponse.headers.get('x-version')
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')
const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
}
}, [userProfileResponse])
@ -115,17 +105,11 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
setCurrentWorkspace(currentWorkspaceResponse)
}, [currentWorkspaceResponse])
if (!appList || !userProfile)
return <Loading type='app' />
return (
<AppContext.Provider value={{
apps: appList.data,
mutateApps,
userProfile,
mutateUserProfile,
pageContainerRef,
langeniusVersionInfo,
langGeniusVersionInfo,
useSelector,
currentWorkspace,
isCurrentWorkspaceManager,
@ -137,7 +121,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
}}>
<div className='flex h-full flex-col overflow-y-auto'>
{globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && <MaintenanceNotice />}
<div ref={pageContainerRef} className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'>
<div className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'>
{children}
</div>
</div>

@ -14,7 +14,7 @@ const client = new QueryClient({
},
})
export const TanstackQueryIniter: FC<PropsWithChildren> = (props) => {
export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => {
const { children } = props
return <QueryClientProvider client={client}>
{children}

@ -8,6 +8,7 @@ import storybook from 'eslint-plugin-storybook'
import tailwind from 'eslint-plugin-tailwindcss'
import reactHooks from 'eslint-plugin-react-hooks'
import sonar from 'eslint-plugin-sonarjs'
import oxlint from 'eslint-plugin-oxlint'
// import reactRefresh from 'eslint-plugin-react-refresh'
@ -245,4 +246,5 @@ export default combine(
'tailwindcss/migration-from-tailwind-2': 'warn',
},
},
oxlint.configs['flat/recommended'],
)

@ -78,7 +78,7 @@ const translation = {
optional: 'Wahlfrei',
noTemplateFound: 'Keine Vorlagen gefunden',
workflowUserDescription: 'Autonome KI-Arbeitsabläufe visuell per Drag-and-Drop erstellen.',
foundResults: '{{Anzahl}} Befund',
foundResults: '{{count}} Befund',
chatbotShortDescription: 'LLM-basierter Chatbot mit einfacher Einrichtung',
completionUserDescription: 'Erstellen Sie schnell einen KI-Assistenten für Textgenerierungsaufgaben mit einfacher Konfiguration.',
noAppsFound: 'Keine Apps gefunden',
@ -92,7 +92,7 @@ const translation = {
noTemplateFoundTip: 'Versuchen Sie, mit verschiedenen Schlüsselwörtern zu suchen.',
advancedUserDescription: 'Workflow mit Speicherfunktionen und Chatbot-Oberfläche.',
chatbotUserDescription: 'Erstellen Sie schnell einen LLM-basierten Chatbot mit einfacher Konfiguration. Sie können später zu Chatflow wechseln.',
foundResult: '{{Anzahl}} Ergebnis',
foundResult: '{{count}} Ergebnis',
agentUserDescription: 'Ein intelligenter Agent, der in der Lage ist, iteratives Denken zu führen und autonome Werkzeuge zu verwenden, um Aufgabenziele zu erreichen.',
agentShortDescription: 'Intelligenter Agent mit logischem Denken und autonomer Werkzeugnutzung',
dropDSLToCreateApp: 'Ziehen Sie die DSL-Datei hierher, um die App zu erstellen',

@ -77,10 +77,10 @@ const translation = {
appCreateDSLErrorPart1: 'تفاوت قابل توجهی در نسخه های DSL مشاهده شده است. اجبار به واردات ممکن است باعث اختلال در عملکرد برنامه شود.',
appCreateDSLWarning: 'احتیاط: تفاوت نسخه DSL ممکن است بر ویژگی های خاصی تأثیر بگذارد',
completionShortDescription: 'دستیار هوش مصنوعی برای تسک های تولید متن',
foundResult: '{{تعداد}} نتیجه',
foundResult: '{{count}} نتیجه',
chatbotUserDescription: 'به سرعت یک چت بات مبتنی بر LLM با پیکربندی ساده بسازید. بعدا می توانید به Chatflow بروید.',
chooseAppType: 'انتخاب نوع برنامه',
foundResults: '{{تعداد}} نتیجه',
foundResults: '{{count}} نتیجه',
noIdeaTip: 'ایده ای ندارید؟ قالب های ما را بررسی کنید',
forBeginners: 'انواع برنامه‌های پایه‌تر',
noAppsFound: 'هیچ برنامه ای یافت نشد',

@ -74,12 +74,12 @@ const translation = {
appCreateDSLErrorPart2: 'क्या आप जारी रखना चाहते हैं?',
learnMore: 'और जानो',
forBeginners: 'नए उपयोगकर्ताओं के लिए बुनियादी ऐप प्रकार',
foundResults: '{{गिनती}} परिणाम',
foundResults: '{{count}} परिणाम',
forAdvanced: 'उन्नत उपयोगकर्ताओं के लिए',
agentUserDescription: 'पुनरावृत्त तर्क और स्वायत्त उपकरण में सक्षम एक बुद्धिमान एजेंट कार्य लक्ष्यों को प्राप्त करने के लिए उपयोग करता है।',
optional: 'वैकल्पिक',
chatbotShortDescription: 'सरल सेटअप के साथ एलएलएम-आधारित चैटबॉट',
foundResult: '{{गिनती}} परिणाम',
foundResult: '{{count}} परिणाम',
completionUserDescription: 'सरल कॉन्फ़िगरेशन के साथ पाठ निर्माण कार्यों के लिए त्वरित रूप से AI सहायक बनाएं।',
noIdeaTip: 'कोई विचार नहीं? हमारे टेम्प्लेट देखें',
noTemplateFound: 'कोई टेम्पलेट नहीं मिला',

@ -79,11 +79,11 @@ const translation = {
appCreateDSLErrorTitle: 'バージョンの非互換性',
appCreateDSLWarning: '注意:DSL のバージョンの違いは、特定の機能に影響を与える可能性があります',
appCreateDSLErrorPart1: 'DSL バージョンに大きな違いが検出されました。インポートを強制すると、アプリケーションが誤動作する可能性があります。',
optional: '意',
optional: '意',
forBeginners: '初心者向けの基本的なアプリタイプ',
noTemplateFoundTip: '別のキーワードを使用して検索してみてください。',
agentShortDescription: '推論と自律的なツールの使用を備えたインテリジェントエージェント',
foundResults: '{{カウント}}業績',
foundResults: '{{count}}件の結果',
noTemplateFound: 'テンプレートが見つかりません',
noAppsFound: 'アプリが見つかりませんでした',
workflowShortDescription: 'インテリジェントな自動化のためのエージェントフロー',
@ -91,7 +91,7 @@ const translation = {
advancedUserDescription: '追加のメモリ機能とチャットボットインターフェースを備えたワークフロー',
advancedShortDescription: 'メモリを使用した複雑なマルチターン対話のワークフロー',
agentUserDescription: 'タスクの目標を達成するために反復的な推論と自律的なツールを使用できるインテリジェントエージェント。',
foundResult: '{{カウント}}結果',
foundResult: '{{count}}件の結果',
forAdvanced: '上級ユーザー向け',
chooseAppType: 'アプリタイプを選択',
learnMore: '詳細情報',

@ -43,7 +43,7 @@ const translation = {
log: 'ログ',
learnMore: '詳細はこちら',
params: 'パラメータ',
duplicate: '複',
duplicate: '',
rename: '名前の変更',
audioSourceUnavailable: 'AudioSource が利用できません',
zoomIn: 'ズームインする',
@ -229,7 +229,7 @@ const translation = {
permanentlyDeleteButton: 'アカウントを完全に削除',
feedbackTitle: 'フィードバック',
feedbackLabel: 'アカウントを削除した理由を教えてください。',
feedbackPlaceholder: '意',
feedbackPlaceholder: '意',
sendVerificationButton: '確認コードの送信',
editWorkspaceInfo: 'ワークスペース情報を編集',
workspaceName: 'ワークスペース名',

@ -2,7 +2,7 @@ const translation = {
category: {
extensions: '拡張機能',
all: 'すべて',
tools: '道具',
tools: 'ツール',
bundles: 'バンドル',
agents: 'エージェント戦略',
models: 'モデル',
@ -11,7 +11,7 @@ const translation = {
agent: 'エージェント戦略',
model: 'モデル',
bundle: 'バンドル',
tool: '道具',
tool: 'ツール',
extension: '拡張',
},
list: {
@ -60,7 +60,7 @@ const translation = {
uninstalledTitle: 'ツールがインストールされていません',
empty: 'ツールを追加するには「+」ボタンをクリックしてください。複数のツールを追加できます。',
paramsTip1: 'LLM 推論パラメータを制御します。',
toolLabel: '道具',
toolLabel: 'ツール',
unsupportedTitle: 'サポートされていないアクション',
toolSetting: 'ツール設定',
unsupportedMCPTool: '現在選択されているエージェント戦略プラグインのバージョンはMCPツールをサポートしていません。',

@ -887,7 +887,7 @@ const translation = {
modelNotSelected: 'モデルが選択されていません',
toolNotAuthorizedTooltip: '{{tool}} 認可されていません',
toolNotInstallTooltip: '{{tool}}はインストールされていません',
tools: '道具',
tools: 'ツール',
learnMore: 'もっと学ぶ',
configureModel: 'モデルを設定する',
model: 'モデル',

@ -90,12 +90,12 @@ const translation = {
noTemplateFound: '템플릿을 찾을 수 없습니다.',
completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미',
learnMore: '더 알아보세요',
foundResults: '{{개수}} 결과',
foundResults: '{{count}} 결과',
agentShortDescription:
'추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트',
advancedShortDescription: '다중 대화를 위해 강화된 워크플로우',
noAppsFound: '앱을 찾을 수 없습니다.',
foundResult: '{{개수}} 결과',
foundResult: '{{count}} 결과',
completionUserDescription:
'간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.',
chatbotUserDescription:

@ -80,7 +80,7 @@ const translation = {
appCreateDSLErrorPart1: 'Wykryto istotną różnicę w wersjach DSL. Wymuszenie importu może spowodować nieprawidłowe działanie aplikacji.',
noTemplateFoundTip: 'Spróbuj wyszukać za pomocą różnych słów kluczowych.',
noAppsFound: 'Nie znaleziono aplikacji',
foundResults: '{{liczba}} Wyniki',
foundResults: '{{count}} Wyniki',
noTemplateFound: 'Nie znaleziono szablonów',
chatbotUserDescription: 'Szybko zbuduj chatbota opartego na LLM z prostą konfiguracją. Możesz przełączyć się na Chatflow później.',
optional: 'Fakultatywny',
@ -91,7 +91,7 @@ const translation = {
completionShortDescription: 'Asystent AI do zadań generowania tekstu',
noIdeaTip: 'Nie masz pomysłów? Sprawdź nasze szablony',
forAdvanced: 'DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW',
foundResult: '{{liczba}} Wynik',
foundResult: '{{count}} Wynik',
advancedShortDescription: 'Przepływ ulepszony dla wieloturowych czatów',
learnMore: 'Dowiedz się więcej',
chatbotShortDescription: 'Chatbot oparty na LLM z prostą konfiguracją',

@ -84,8 +84,8 @@ const translation = {
advancedShortDescription: 'Flux de lucru îmbunătățit pentru conversații multi-tur',
advancedUserDescription: 'Flux de lucru cu funcții suplimentare de memorie și interfață de chatbot.',
noTemplateFoundTip: 'Încercați să căutați folosind cuvinte cheie diferite.',
foundResults: '{{număr}} Rezultatele',
foundResult: '{{număr}} Rezultat',
foundResults: '{{count}} Rezultatele',
foundResult: '{{count}} Rezultat',
noIdeaTip: 'Nicio idee? Consultați șabloanele noastre',
noAppsFound: 'Nu s-au găsit aplicații',
workflowShortDescription: 'Flux agentic pentru automatizări inteligente',

@ -78,11 +78,11 @@ const translation = {
appCreateDSLErrorPart1: 'Обнаружена существенная разница в версиях DSL. Принудительный импорт может привести к сбою в работе приложения.',
learnMore: 'Подробнее',
forAdvanced: 'ДЛЯ ПРОДВИНУТЫХ ПОЛЬЗОВАТЕЛЕЙ',
foundResults: '{{Количество}} Результаты',
foundResults: '{{count}} Результаты',
optional: 'Необязательный',
chatbotShortDescription: 'Чат-бот на основе LLM с простой настройкой',
advancedShortDescription: 'Рабочий процесс, улучшенный для многоходовых чатов',
foundResult: '{{Количество}} Результат',
foundResult: '{{count}} Результат',
workflowShortDescription: 'Агентный поток для интеллектуальных автоматизаций',
advancedUserDescription: 'Рабочий процесс с дополнительными функциями памяти и интерфейсом чат-бота.',
noAppsFound: 'Приложения не найдены',

@ -79,8 +79,8 @@ const translation = {
advancedShortDescription: 'Potek dela izboljšan za večkratne pogovore',
noAppsFound: 'Ni bilo najdenih aplikacij',
agentShortDescription: 'Inteligentni agent z razmišljanjem in avtonomno uporabo orodij',
foundResult: '{{štetje}} Rezultat',
foundResults: '{{štetje}} Rezultati',
foundResult: '{{count}} Rezultat',
foundResults: '{{count}} Rezultati',
noTemplateFoundTip: 'Poskusite iskati z različnimi ključnimi besedami.',
optional: 'Neobvezno',
forBeginners: 'Bolj osnovne vrste aplikacij',

@ -73,7 +73,7 @@ const translation = {
appCreateDSLErrorPart4: 'เวอร์ชัน DSL ที่ระบบรองรับ:',
appCreateFailed: 'สร้างโปรเจกต์ไม่สําเร็จ',
learnMore: 'ศึกษาเพิ่มเติม',
foundResults: '{{นับ}} ผลลัพธ์',
foundResults: '{{count}} ผลลัพธ์',
noTemplateFoundTip: 'ลองค้นหาโดยใช้คีย์เวิร์ดอื่น',
chatbotShortDescription: 'แชทบอทที่ใช้ LLM พร้อมการตั้งค่าที่ง่ายดาย',
optional: 'เสริม',
@ -83,7 +83,7 @@ const translation = {
completionShortDescription: 'ผู้ช่วย AI สําหรับงานสร้างข้อความ',
agentUserDescription: 'ตัวแทนอัจฉริยะที่สามารถให้เหตุผลซ้ําๆ และใช้เครื่องมืออัตโนมัติเพื่อให้บรรลุเป้าหมายของงาน',
noIdeaTip: 'ไม่มีความคิด? ดูเทมเพลตของเรา',
foundResult: '{{นับ}} ผล',
foundResult: '{{count}} ผล',
noAppsFound: 'ไม่พบแอป',
workflowShortDescription: 'โฟลว์อัตโนมัติสำหรับระบบอัจฉริยะ',
forAdvanced: 'สําหรับผู้ใช้ขั้นสูง',

@ -72,11 +72,11 @@ const translation = {
appCreateDSLErrorPart3: 'Geçerli uygulama DSL sürümü:',
appCreateDSLErrorTitle: 'Sürüm Uyumsuzluğu',
Confirm: 'Onaylamak',
foundResults: '{{sayı}} Sonuç -ları',
foundResults: '{{count}} Sonuç -ları',
noAppsFound: 'Uygulama bulunamadı',
chatbotUserDescription: 'Basit yapılandırmayla hızlı bir şekilde LLM tabanlı bir sohbet botu oluşturun. Daha sonra Chatflow\'a geçebilirsiniz.',
optional: 'Opsiyonel',
foundResult: '{{sayı}} Sonuç',
foundResult: '{{count}} Sonuç',
noTemplateFound: 'Şablon bulunamadı',
workflowUserDescription: 'Sürükle-bırak kolaylığıyla görsel olarak otonom yapay zeka iş akışları oluşturun.',
advancedUserDescription: 'Ek bellek özellikleri ve sohbet robotu arayüzü ile iş akışı.',

@ -80,13 +80,13 @@ const translation = {
optional: 'Tùy chọn',
advancedShortDescription: 'Quy trình làm việc cho các cuộc đối thoại nhiều lượt phức tạp với bộ nhớ',
workflowUserDescription: 'Xây dựng trực quan quy trình AI tự động bằng kéo thả đơn giản.',
foundResults: '{{đếm}} Kết quả',
foundResults: '{{count}} Kết quả',
chatbotUserDescription: 'Nhanh chóng xây dựng chatbot dựa trên LLM với cấu hình đơn giản. Bạn có thể chuyển sang Chatflow sau.',
agentUserDescription: 'Một tác nhân thông minh có khả năng suy luận lặp đi lặp lại và sử dụng công cụ tự động để đạt được mục tiêu nhiệm vụ.',
noIdeaTip: 'Không có ý tưởng? Kiểm tra các mẫu của chúng tôi',
advancedUserDescription: 'Quy trình với tính năng bộ nhớ bổ sung và giao diện chatbot.',
forAdvanced: 'DÀNH CHO NGƯỜI DÙNG NÂNG CAO',
foundResult: '{{đếm}} Kết quả',
foundResult: '{{count}} Kết quả',
agentShortDescription: 'Quy trình nâng cao cho hội thoại nhiều lượt',
noTemplateFound: 'Không tìm thấy mẫu',
noAppsFound: 'Không tìm thấy ứng dụng nào',

@ -21,8 +21,8 @@
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js",
"lint": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
"lint-only-show-error": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
"lint": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
"lint-only-show-error": "pnpx oxlint && pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
"fix": "next lint --fix",
"eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
"eslint-fix-only-show-error": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix --quiet",
@ -198,6 +198,7 @@
"cross-env": "^7.0.3",
"eslint": "^9.20.1",
"eslint-config-next": "~15.3.5",
"eslint-plugin-oxlint": "^1.6.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-sonarjs": "^3.0.2",

@ -516,6 +516,9 @@ importers:
eslint-config-next:
specifier: ~15.3.5
version: 15.3.5(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3)
eslint-plugin-oxlint:
specifier: ^1.6.0
version: 1.6.0
eslint-plugin-react-hooks:
specifier: ^5.1.0
version: 5.2.0(eslint@9.31.0(jiti@1.21.7))
@ -4775,6 +4778,9 @@ packages:
resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==}
engines: {node: '>=5.0.0'}
eslint-plugin-oxlint@1.6.0:
resolution: {integrity: sha512-DH5p3sCf0nIAPscl3yGnBWXXraV0bdl66hpLxvfnabvg/GzpgXf+pOCWpGK3qDb0+AIUkh1R/7A8GkOXtlj0oA==}
eslint-plugin-perfectionist@4.15.0:
resolution: {integrity: sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -5832,6 +5838,9 @@ packages:
resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@ -13169,6 +13178,10 @@ snapshots:
eslint-plugin-no-only-tests@3.3.0: {}
eslint-plugin-oxlint@1.6.0:
dependencies:
jsonc-parser: 3.3.1
eslint-plugin-perfectionist@4.15.0(eslint@9.31.0(jiti@1.21.7))(typescript@5.8.3):
dependencies:
'@typescript-eslint/types': 8.37.0
@ -14650,6 +14663,8 @@ snapshots:
espree: 9.6.1
semver: 7.7.2
jsonc-parser@3.3.1: {}
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1

@ -88,7 +88,7 @@ export const logout: Fetcher<CommonResponse, { url: string; params: Record<strin
return get<CommonResponse>(url, params)
}
export const fetchLanggeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const fetchLangGeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<LangGeniusVersionResponse>(url, { params })
}

Loading…
Cancel
Save