mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/hitl-frontend
This commit is contained in:
commit
ca58055a39
|
|
@ -0,0 +1 @@
|
|||
../.claude/skills
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
name: orpc-contract-first
|
||||
description: Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.
|
||||
---
|
||||
|
||||
# oRPC Contract-First Development
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
web/contract/
|
||||
├── base.ts # Base contract (inputStructure: 'detailed')
|
||||
├── router.ts # Router composition & type exports
|
||||
├── marketplace.ts # Marketplace contracts
|
||||
└── console/ # Console contracts by domain
|
||||
├── system.ts
|
||||
└── billing.ts
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Create contract** in `web/contract/console/{domain}.ts`
|
||||
- Import `base` from `../base` and `type` from `@orpc/contract`
|
||||
- Define route with `path`, `method`, `input`, `output`
|
||||
|
||||
2. **Register in router** at `web/contract/router.ts`
|
||||
- Import directly from domain file (no barrel files)
|
||||
- Nest by API prefix: `billing: { invoices, bindPartnerStack }`
|
||||
|
||||
3. **Create hooks** in `web/service/use-{domain}.ts`
|
||||
- Use `consoleQuery.{group}.{contract}.queryKey()` for query keys
|
||||
- Use `consoleClient.{group}.{contract}()` for API calls
|
||||
|
||||
## Key Rules
|
||||
|
||||
- **Input structure**: Always use `{ params, query?, body? }` format
|
||||
- **Path params**: Use `{paramName}` in path, match in `params` object
|
||||
- **Router nesting**: Group by API prefix (e.g., `/billing/*` → `billing: {}`)
|
||||
- **No barrel files**: Import directly from specific files
|
||||
- **Types**: Import from `@/types/`, use `type<T>()` helper
|
||||
|
||||
## Type Export
|
||||
|
||||
```typescript
|
||||
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
||||
```
|
||||
|
|
@ -417,6 +417,8 @@ SMTP_USERNAME=123
|
|||
SMTP_PASSWORD=abc
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_OPPORTUNISTIC_TLS=false
|
||||
# Optional: override the local hostname used for SMTP HELO/EHLO
|
||||
SMTP_LOCAL_HOSTNAME=
|
||||
# Sendgid configuration
|
||||
SENDGRID_API_KEY=
|
||||
# Sentry configuration
|
||||
|
|
@ -713,3 +715,4 @@ ANNOTATION_IMPORT_MAX_CONCURRENT=5
|
|||
SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21
|
||||
SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000
|
||||
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
||||
|
||||
|
|
|
|||
|
|
@ -949,6 +949,12 @@ class MailConfig(BaseSettings):
|
|||
default=False,
|
||||
)
|
||||
|
||||
SMTP_LOCAL_HOSTNAME: str | None = Field(
|
||||
description="Override the local hostname used in SMTP HELO/EHLO. "
|
||||
"Useful behind NAT or when the default hostname causes rejections.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
EMAIL_SEND_IP_LIMIT_PER_MINUTE: PositiveInt = Field(
|
||||
description="Maximum number of emails allowed to be sent from the same IP address in a minute",
|
||||
default=50,
|
||||
|
|
|
|||
|
|
@ -592,9 +592,12 @@ def _get_conversation(app_model, conversation_id):
|
|||
if not conversation:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
||||
if not conversation.read_at:
|
||||
conversation.read_at = naive_utc_now()
|
||||
conversation.read_account_id = current_user.id
|
||||
db.session.commit()
|
||||
db.session.execute(
|
||||
sa.update(Conversation)
|
||||
.where(Conversation.id == conversation_id, Conversation.read_at.is_(None))
|
||||
.values(read_at=naive_utc_now(), read_account_id=current_user.id)
|
||||
)
|
||||
db.session.commit()
|
||||
db.session.refresh(conversation)
|
||||
|
||||
return conversation
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class ExternalKnowledgeApiPayload(BaseModel):
|
|||
class ExternalDatasetCreatePayload(BaseModel):
|
||||
external_knowledge_api_id: str
|
||||
external_knowledge_id: str
|
||||
name: str = Field(..., min_length=1, max_length=40)
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: str | None = Field(None, max_length=400)
|
||||
external_retrieval_model: dict[str, object] | None = None
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ class MaxRetriesExceededError(ValueError):
|
|||
pass
|
||||
|
||||
|
||||
request_error = httpx.RequestError
|
||||
max_retries_exceeded_error = MaxRetriesExceededError
|
||||
|
||||
|
||||
def _create_proxy_mounts() -> dict[str, httpx.HTTPTransport]:
|
||||
return {
|
||||
"http://": httpx.HTTPTransport(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from core.helper import ssrf_proxy
|
|||
from core.variables.segments import ArrayFileSegment, FileSegment
|
||||
from core.workflow.runtime import VariablePool
|
||||
|
||||
from ..protocols import FileManagerProtocol, HttpClientProtocol
|
||||
from .entities import (
|
||||
HttpRequestNodeAuthorization,
|
||||
HttpRequestNodeData,
|
||||
|
|
@ -78,6 +79,8 @@ class Executor:
|
|||
timeout: HttpRequestNodeTimeout,
|
||||
variable_pool: VariablePool,
|
||||
max_retries: int = dify_config.SSRF_DEFAULT_MAX_RETRIES,
|
||||
http_client: HttpClientProtocol = ssrf_proxy,
|
||||
file_manager: FileManagerProtocol = file_manager,
|
||||
):
|
||||
# If authorization API key is present, convert the API key using the variable pool
|
||||
if node_data.authorization.type == "api-key":
|
||||
|
|
@ -104,6 +107,8 @@ class Executor:
|
|||
self.data = None
|
||||
self.json = None
|
||||
self.max_retries = max_retries
|
||||
self._http_client = http_client
|
||||
self._file_manager = file_manager
|
||||
|
||||
# init template
|
||||
self.variable_pool = variable_pool
|
||||
|
|
@ -200,7 +205,7 @@ class Executor:
|
|||
if file_variable is None:
|
||||
raise FileFetchError(f"cannot fetch file with selector {file_selector}")
|
||||
file = file_variable.value
|
||||
self.content = file_manager.download(file)
|
||||
self.content = self._file_manager.download(file)
|
||||
case "x-www-form-urlencoded":
|
||||
form_data = {
|
||||
self.variable_pool.convert_template(item.key).text: self.variable_pool.convert_template(
|
||||
|
|
@ -239,7 +244,7 @@ class Executor:
|
|||
):
|
||||
file_tuple = (
|
||||
file.filename,
|
||||
file_manager.download(file),
|
||||
self._file_manager.download(file),
|
||||
file.mime_type or "application/octet-stream",
|
||||
)
|
||||
if key not in files:
|
||||
|
|
@ -332,19 +337,18 @@ class Executor:
|
|||
do http request depending on api bundle
|
||||
"""
|
||||
_METHOD_MAP = {
|
||||
"get": ssrf_proxy.get,
|
||||
"head": ssrf_proxy.head,
|
||||
"post": ssrf_proxy.post,
|
||||
"put": ssrf_proxy.put,
|
||||
"delete": ssrf_proxy.delete,
|
||||
"patch": ssrf_proxy.patch,
|
||||
"get": self._http_client.get,
|
||||
"head": self._http_client.head,
|
||||
"post": self._http_client.post,
|
||||
"put": self._http_client.put,
|
||||
"delete": self._http_client.delete,
|
||||
"patch": self._http_client.patch,
|
||||
}
|
||||
method_lc = self.method.lower()
|
||||
if method_lc not in _METHOD_MAP:
|
||||
raise InvalidHttpMethodError(f"Invalid http method {self.method}")
|
||||
|
||||
request_args = {
|
||||
"url": self.url,
|
||||
"data": self.data,
|
||||
"files": self.files,
|
||||
"json": self.json,
|
||||
|
|
@ -357,8 +361,12 @@ class Executor:
|
|||
}
|
||||
# request_args = {k: v for k, v in request_args.items() if v is not None}
|
||||
try:
|
||||
response: httpx.Response = _METHOD_MAP[method_lc](**request_args, max_retries=self.max_retries)
|
||||
except (ssrf_proxy.MaxRetriesExceededError, httpx.RequestError) as e:
|
||||
response: httpx.Response = _METHOD_MAP[method_lc](
|
||||
url=self.url,
|
||||
**request_args,
|
||||
max_retries=self.max_retries,
|
||||
)
|
||||
except (self._http_client.max_retries_exceeded_error, self._http_client.request_error) as e:
|
||||
raise HttpRequestNodeError(str(e)) from e
|
||||
# FIXME: fix type ignore, this maybe httpx type issue
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import logging
|
||||
import mimetypes
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from configs import dify_config
|
||||
from core.file import File, FileTransferMethod
|
||||
from core.file import File, FileTransferMethod, file_manager
|
||||
from core.helper import ssrf_proxy
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from core.variables.segments import ArrayFileSegment
|
||||
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
|
||||
|
|
@ -13,6 +14,7 @@ from core.workflow.nodes.base import variable_template_parser
|
|||
from core.workflow.nodes.base.entities import VariableSelector
|
||||
from core.workflow.nodes.base.node import Node
|
||||
from core.workflow.nodes.http_request.executor import Executor
|
||||
from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol
|
||||
from factories import file_factory
|
||||
|
||||
from .entities import (
|
||||
|
|
@ -30,10 +32,35 @@ HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout(
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.workflow.entities import GraphInitParams
|
||||
from core.workflow.runtime import GraphRuntimeState
|
||||
|
||||
|
||||
class HttpRequestNode(Node[HttpRequestNodeData]):
|
||||
node_type = NodeType.HTTP_REQUEST
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
config: Mapping[str, Any],
|
||||
graph_init_params: "GraphInitParams",
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
*,
|
||||
http_client: HttpClientProtocol = ssrf_proxy,
|
||||
tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
|
||||
file_manager: FileManagerProtocol = file_manager,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
id=id,
|
||||
config=config,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
)
|
||||
self._http_client = http_client
|
||||
self._tool_file_manager_factory = tool_file_manager_factory
|
||||
self._file_manager = file_manager
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]:
|
||||
return {
|
||||
|
|
@ -71,6 +98,8 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
|
|||
timeout=self._get_request_timeout(self.node_data),
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
max_retries=0,
|
||||
http_client=self._http_client,
|
||||
file_manager=self._file_manager,
|
||||
)
|
||||
process_data["request"] = http_executor.to_log()
|
||||
|
||||
|
|
@ -199,7 +228,7 @@ class HttpRequestNode(Node[HttpRequestNodeData]):
|
|||
mime_type = (
|
||||
content_disposition_type or content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
)
|
||||
tool_file_manager = ToolFileManager()
|
||||
tool_file_manager = self._tool_file_manager_factory()
|
||||
|
||||
tool_file = tool_file_manager.create_file_by_raw(
|
||||
user_id=self.user_id,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import TYPE_CHECKING, final
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from configs import dify_config
|
||||
from core.file import file_manager
|
||||
from core.helper import ssrf_proxy
|
||||
from core.helper.code_executor.code_executor import CodeExecutor
|
||||
from core.helper.code_executor.code_node_provider import CodeNodeProvider
|
||||
from core.tools.tool_file_manager import ToolFileManager
|
||||
from core.workflow.enums import NodeType
|
||||
from core.workflow.graph import NodeFactory
|
||||
from core.workflow.nodes.base.node import Node
|
||||
from core.workflow.nodes.code.code_node import CodeNode
|
||||
from core.workflow.nodes.code.limits import CodeNodeLimits
|
||||
from core.workflow.nodes.http_request.node import HttpRequestNode
|
||||
from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol
|
||||
from core.workflow.nodes.template_transform.template_renderer import (
|
||||
CodeExecutorJinja2TemplateRenderer,
|
||||
Jinja2TemplateRenderer,
|
||||
|
|
@ -43,6 +48,9 @@ class DifyNodeFactory(NodeFactory):
|
|||
code_providers: Sequence[type[CodeNodeProvider]] | None = None,
|
||||
code_limits: CodeNodeLimits | None = None,
|
||||
template_renderer: Jinja2TemplateRenderer | None = None,
|
||||
http_request_http_client: HttpClientProtocol = ssrf_proxy,
|
||||
http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
|
||||
http_request_file_manager: FileManagerProtocol = file_manager,
|
||||
) -> None:
|
||||
self.graph_init_params = graph_init_params
|
||||
self.graph_runtime_state = graph_runtime_state
|
||||
|
|
@ -61,6 +69,9 @@ class DifyNodeFactory(NodeFactory):
|
|||
max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH,
|
||||
)
|
||||
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer()
|
||||
self._http_request_http_client = http_request_http_client
|
||||
self._http_request_tool_file_manager_factory = http_request_tool_file_manager_factory
|
||||
self._http_request_file_manager = http_request_file_manager
|
||||
|
||||
@override
|
||||
def create_node(self, node_config: dict[str, object]) -> Node:
|
||||
|
|
@ -113,6 +124,7 @@ class DifyNodeFactory(NodeFactory):
|
|||
code_providers=self._code_providers,
|
||||
code_limits=self._code_limits,
|
||||
)
|
||||
|
||||
if node_type == NodeType.TEMPLATE_TRANSFORM:
|
||||
return TemplateTransformNode(
|
||||
id=node_id,
|
||||
|
|
@ -122,6 +134,17 @@ class DifyNodeFactory(NodeFactory):
|
|||
template_renderer=self._template_renderer,
|
||||
)
|
||||
|
||||
if node_type == NodeType.HTTP_REQUEST:
|
||||
return HttpRequestNode(
|
||||
id=node_id,
|
||||
config=node_config,
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
http_client=self._http_request_http_client,
|
||||
tool_file_manager_factory=self._http_request_tool_file_manager_factory,
|
||||
file_manager=self._http_request_file_manager,
|
||||
)
|
||||
|
||||
return node_class(
|
||||
id=node_id,
|
||||
config=node_config,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
from typing import Protocol
|
||||
|
||||
import httpx
|
||||
|
||||
from core.file import File
|
||||
|
||||
|
||||
class HttpClientProtocol(Protocol):
|
||||
@property
|
||||
def max_retries_exceeded_error(self) -> type[Exception]: ...
|
||||
|
||||
@property
|
||||
def request_error(self) -> type[Exception]: ...
|
||||
|
||||
def get(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
def head(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
def post(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
def put(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
def delete(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
def patch(self, url: str, max_retries: int = ..., **kwargs: object) -> httpx.Response: ...
|
||||
|
||||
|
||||
class FileManagerProtocol(Protocol):
|
||||
def download(self, f: File, /) -> bytes: ...
|
||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from datetime import datetime
|
||||
from typing import TypeAlias
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
|
@ -20,8 +21,8 @@ class SimpleFeedback(ResponseModel):
|
|||
|
||||
|
||||
class RetrieverResource(ResponseModel):
|
||||
id: str
|
||||
message_id: str
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
message_id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
position: int
|
||||
dataset_id: str | None = None
|
||||
dataset_name: str | None = None
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import smtplib
|
|||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from configs import dify_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -19,20 +21,21 @@ class SMTPClient:
|
|||
self.opportunistic_tls = opportunistic_tls
|
||||
|
||||
def send(self, mail: dict):
|
||||
smtp = None
|
||||
smtp: smtplib.SMTP | None = None
|
||||
local_host = dify_config.SMTP_LOCAL_HOSTNAME
|
||||
try:
|
||||
if self.use_tls:
|
||||
if self.opportunistic_tls:
|
||||
smtp = smtplib.SMTP(self.server, self.port, timeout=10)
|
||||
# Send EHLO command with the HELO domain name as the server address
|
||||
smtp.ehlo(self.server)
|
||||
smtp.starttls()
|
||||
# Resend EHLO command to identify the TLS session
|
||||
smtp.ehlo(self.server)
|
||||
else:
|
||||
smtp = smtplib.SMTP_SSL(self.server, self.port, timeout=10)
|
||||
if self.use_tls and not self.opportunistic_tls:
|
||||
# SMTP with SSL (implicit TLS)
|
||||
smtp = smtplib.SMTP_SSL(self.server, self.port, timeout=10, local_hostname=local_host)
|
||||
else:
|
||||
smtp = smtplib.SMTP(self.server, self.port, timeout=10)
|
||||
# Plain SMTP or SMTP with STARTTLS (explicit TLS)
|
||||
smtp = smtplib.SMTP(self.server, self.port, timeout=10, local_hostname=local_host)
|
||||
|
||||
assert smtp is not None
|
||||
if self.use_tls and self.opportunistic_tls:
|
||||
smtp.ehlo(self.server)
|
||||
smtp.starttls()
|
||||
smtp.ehlo(self.server)
|
||||
|
||||
# Only authenticate if both username and password are non-empty
|
||||
if self.username and self.password and self.username.strip() and self.password.strip():
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ SMTP_USERNAME=123
|
|||
SMTP_PASSWORD=abc
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_OPPORTUNISTIC_TLS=false
|
||||
# Optional: override the local hostname used for SMTP HELO/EHLO
|
||||
SMTP_LOCAL_HOSTNAME=
|
||||
|
||||
# Sentry configuration
|
||||
SENTRY_DSN=
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""Unit tests for `controllers.console.datasets` controllers."""
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
from __future__ import annotations
|
||||
|
||||
"""
|
||||
Unit tests for the external dataset controller payload schemas.
|
||||
|
||||
These tests focus on Pydantic validation rules so we can catch regressions
|
||||
in request constraints (e.g. max length changes) without exercising the
|
||||
full Flask/RESTX request stack.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from controllers.console.datasets.external import ExternalDatasetCreatePayload
|
||||
|
||||
|
||||
def test_external_dataset_create_payload_allows_name_length_100() -> None:
|
||||
"""Ensure the `name` field accepts up to 100 characters (inclusive)."""
|
||||
|
||||
# Build a request payload with a boundary-length name value.
|
||||
name_100: str = "a" * 100
|
||||
payload = {
|
||||
"external_knowledge_api_id": "ek-api-1",
|
||||
"external_knowledge_id": "ek-1",
|
||||
"name": name_100,
|
||||
}
|
||||
|
||||
model = ExternalDatasetCreatePayload.model_validate(payload)
|
||||
assert model.name == name_100
|
||||
|
||||
|
||||
def test_external_dataset_create_payload_rejects_name_length_101() -> None:
|
||||
"""Ensure the `name` field rejects values longer than 100 characters."""
|
||||
|
||||
# Build a request payload that exceeds the max length by 1.
|
||||
name_101: str = "a" * 101
|
||||
payload: dict[str, object] = {
|
||||
"external_knowledge_api_id": "ek-api-1",
|
||||
"external_knowledge_id": "ek-1",
|
||||
"name": name_101,
|
||||
}
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ExternalDatasetCreatePayload.model_validate(payload)
|
||||
|
||||
errors = exc_info.value.errors()
|
||||
assert errors[0]["loc"] == ("name",)
|
||||
assert errors[0]["type"] == "string_too_long"
|
||||
assert errors[0]["ctx"]["max_length"] == 100
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ def test_smtp_plain_success(mock_smtp_cls: MagicMock):
|
|||
client = SMTPClient(server="smtp.example.com", port=25, username="", password="", _from="noreply@example.com")
|
||||
client.send(_mail())
|
||||
|
||||
mock_smtp_cls.assert_called_once_with("smtp.example.com", 25, timeout=10)
|
||||
mock_smtp_cls.assert_called_once_with("smtp.example.com", 25, timeout=10, local_hostname=ANY)
|
||||
mock_smtp.sendmail.assert_called_once()
|
||||
mock_smtp.quit.assert_called_once()
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ def test_smtp_tls_opportunistic_success(mock_smtp_cls: MagicMock):
|
|||
)
|
||||
client.send(_mail())
|
||||
|
||||
mock_smtp_cls.assert_called_once_with("smtp.example.com", 587, timeout=10)
|
||||
mock_smtp_cls.assert_called_once_with("smtp.example.com", 587, timeout=10, local_hostname=ANY)
|
||||
assert mock_smtp.ehlo.call_count == 2
|
||||
mock_smtp.starttls.assert_called_once()
|
||||
mock_smtp.login.assert_called_once_with("user", "pass")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ This module tests the mail sending functionality including:
|
|||
"""
|
||||
|
||||
import smtplib
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ class TestSMTPIntegration:
|
|||
client.send(mail_data)
|
||||
|
||||
# Assert
|
||||
mock_smtp_ssl.assert_called_once_with("smtp.example.com", 465, timeout=10)
|
||||
mock_smtp_ssl.assert_called_once_with("smtp.example.com", 465, timeout=10, local_hostname=ANY)
|
||||
mock_server.login.assert_called_once_with("user@example.com", "password123")
|
||||
mock_server.sendmail.assert_called_once()
|
||||
mock_server.quit.assert_called_once()
|
||||
|
|
@ -181,7 +181,7 @@ class TestSMTPIntegration:
|
|||
client.send(mail_data)
|
||||
|
||||
# Assert
|
||||
mock_smtp.assert_called_once_with("smtp.example.com", 587, timeout=10)
|
||||
mock_smtp.assert_called_once_with("smtp.example.com", 587, timeout=10, local_hostname=ANY)
|
||||
mock_server.ehlo.assert_called()
|
||||
mock_server.starttls.assert_called_once()
|
||||
assert mock_server.ehlo.call_count == 2 # Before and after STARTTLS
|
||||
|
|
@ -213,7 +213,7 @@ class TestSMTPIntegration:
|
|||
client.send(mail_data)
|
||||
|
||||
# Assert
|
||||
mock_smtp.assert_called_once_with("smtp.example.com", 25, timeout=10)
|
||||
mock_smtp.assert_called_once_with("smtp.example.com", 25, timeout=10, local_hostname=ANY)
|
||||
mock_server.login.assert_called_once()
|
||||
mock_server.sendmail.assert_called_once()
|
||||
mock_server.quit.assert_called_once()
|
||||
|
|
|
|||
4664
api/uv.lock
4664
api/uv.lock
File diff suppressed because it is too large
Load Diff
|
|
@ -968,6 +968,8 @@ SMTP_USERNAME=
|
|||
SMTP_PASSWORD=
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_OPPORTUNISTIC_TLS=false
|
||||
# Optional: override the local hostname used for SMTP HELO/EHLO
|
||||
SMTP_LOCAL_HOSTNAME=
|
||||
|
||||
# Sendgid configuration
|
||||
SENDGRID_API_KEY=
|
||||
|
|
|
|||
|
|
@ -425,6 +425,7 @@ x-shared-env: &shared-api-worker-env
|
|||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||
SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false}
|
||||
SMTP_LOCAL_HOSTNAME: ${SMTP_LOCAL_HOSTNAME:-}
|
||||
SENDGRID_API_KEY: ${SENDGRID_API_KEY:-}
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
||||
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ export default function CheckCode() {
|
|||
setIsLoading(true)
|
||||
const ret = await webAppEmailLoginWithCode({ email, code: encryptVerificationCode(code), token })
|
||||
if (ret.result === 'success') {
|
||||
setWebAppAccessToken(ret.data.access_token)
|
||||
if (ret?.data?.access_token) {
|
||||
setWebAppAccessToken(ret.data.access_token)
|
||||
}
|
||||
const { access_token } = await fetchAccessToken({
|
||||
appCode: appCode!,
|
||||
userId: embeddedUserId || undefined,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
|||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
if (res?.data?.access_token) {
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
}
|
||||
|
||||
const { access_token } = await fetchAccessToken({
|
||||
appCode: appCode!,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,6 @@ const AgentTools: FC = () => {
|
|||
onSelect={handleSelectTool}
|
||||
onSelectMultiple={handleSelectMultipleTool}
|
||||
selectedTools={tools as unknown as ToolValue[]}
|
||||
canChooseMCPTool
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -362,6 +362,18 @@ describe('PreviewDocumentPicker', () => {
|
|||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when value prop is omitted (optional)', () => {
|
||||
const files = createMockDocumentList(2)
|
||||
const onChange = vi.fn()
|
||||
// Do not pass `value` at all to verify optional behavior
|
||||
render(<PreviewDocumentPicker files={files} onChange={onChange} />)
|
||||
|
||||
// Renders placeholder for missing name
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
// Portal wrapper renders
|
||||
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty files array', () => {
|
||||
renderComponent({ files: [] })
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import DocumentList from './document-list'
|
|||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: DocumentItem
|
||||
value?: DocumentItem
|
||||
files: DocumentItem[]
|
||||
onChange: (value: DocumentItem) => void
|
||||
}
|
||||
|
|
@ -30,7 +30,8 @@ const PreviewDocumentPicker: FC<Props> = ({
|
|||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, extension } = value
|
||||
const name = value?.name || ''
|
||||
const extension = value?.extension
|
||||
|
||||
const [open, {
|
||||
set: setOpen,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
|
|||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const docLink = useDocLink('https://docs.dify.ai/versions/3-0-x')
|
||||
const [localeData, setLocaleData] = useState(data)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { notify } = useToastContext()
|
||||
|
|
@ -102,7 +102,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
|
|||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.apiEndpoint.title', { ns: 'common' })}
|
||||
<a
|
||||
href={docLink('/guides/extension/api-based-extension/README')}
|
||||
href={docLink('/user-guide/extension/api-based-extension/README#api-based-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs font-normal text-text-accent"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ type FormProps<
|
|||
nodeId?: string
|
||||
nodeOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
function Form<
|
||||
|
|
@ -81,7 +80,6 @@ function Form<
|
|||
nodeId,
|
||||
nodeOutputVars,
|
||||
availableNodes,
|
||||
canChooseMCPTool,
|
||||
}: FormProps<CustomFormSchema>) {
|
||||
const language = useLanguage()
|
||||
const [changeKey, setChangeKey] = useState('')
|
||||
|
|
@ -407,7 +405,6 @@ function Form<
|
|||
value={value[variable] || []}
|
||||
onChange={item => handleFormChange(variable, item as any)}
|
||||
supportCollapse
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
{fieldMoreInfo?.(formSchema)}
|
||||
{validating && changeKey === variable && <ValidatingTip />}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
|
||||
// ==================== Imports (after mocks) ====================
|
||||
|
||||
import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import MultipleToolSelector from './index'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
|
@ -190,10 +191,11 @@ type RenderOptions = {
|
|||
nodeOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
nodeId?: string
|
||||
canChooseMCPTool?: boolean
|
||||
versionSupported?: boolean
|
||||
}
|
||||
|
||||
const renderComponent = (options: RenderOptions = {}) => {
|
||||
const { versionSupported, ...overrides } = options
|
||||
const defaultProps = {
|
||||
disabled: false,
|
||||
value: [],
|
||||
|
|
@ -206,16 +208,17 @@ const renderComponent = (options: RenderOptions = {}) => {
|
|||
nodeOutputVars: [createNodeOutputVar()],
|
||||
availableNodes: [createNode()],
|
||||
nodeId: 'test-node-id',
|
||||
canChooseMCPTool: false,
|
||||
}
|
||||
|
||||
const props = { ...defaultProps, ...options }
|
||||
const props = { ...defaultProps, ...overrides }
|
||||
const queryClient = createQueryClient()
|
||||
|
||||
return {
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MultipleToolSelector {...props} />
|
||||
<MCPToolAvailabilityProvider versionSupported={versionSupported}>
|
||||
<MultipleToolSelector {...props} />
|
||||
</MCPToolAvailabilityProvider>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
props,
|
||||
|
|
@ -410,7 +413,7 @@ describe('MultipleToolSelector', () => {
|
|||
expect(screen.getByText('2/3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should track enabled count with MCP tools when canChooseMCPTool is true', () => {
|
||||
it('should track enabled count with MCP tools when version is supported', () => {
|
||||
// Arrange
|
||||
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
|
||||
mockMCPToolsData.mockReturnValue(mcpTools)
|
||||
|
|
@ -421,13 +424,13 @@ describe('MultipleToolSelector', () => {
|
|||
]
|
||||
|
||||
// Act
|
||||
renderComponent({ value: tools, canChooseMCPTool: true })
|
||||
renderComponent({ value: tools, versionSupported: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('2/2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not count MCP tools when canChooseMCPTool is false', () => {
|
||||
it('should not count MCP tools when version is unsupported', () => {
|
||||
// Arrange
|
||||
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
|
||||
mockMCPToolsData.mockReturnValue(mcpTools)
|
||||
|
|
@ -438,7 +441,7 @@ describe('MultipleToolSelector', () => {
|
|||
]
|
||||
|
||||
// Act
|
||||
renderComponent({ value: tools, canChooseMCPTool: false })
|
||||
renderComponent({ value: tools, versionSupported: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('1/2')).toBeInTheDocument()
|
||||
|
|
@ -721,14 +724,6 @@ describe('MultipleToolSelector', () => {
|
|||
expect(screen.getByTestId('tool-selector-add')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass canChooseMCPTool prop correctly', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ canChooseMCPTool: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('tool-selector-add')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with supportEnableSwitch for edit selectors', () => {
|
||||
// Arrange
|
||||
const tools = [createToolValue()]
|
||||
|
|
@ -771,13 +766,13 @@ describe('MultipleToolSelector', () => {
|
|||
]
|
||||
|
||||
// Act
|
||||
renderComponent({ value: tools, canChooseMCPTool: true })
|
||||
renderComponent({ value: tools, versionSupported: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('2/2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should exclude MCP tools from enabled count when canChooseMCPTool is false', () => {
|
||||
it('should exclude MCP tools from enabled count when strategy version is unsupported', () => {
|
||||
// Arrange
|
||||
const mcpTools = [createMCPTool({ id: 'mcp-provider' })]
|
||||
mockMCPToolsData.mockReturnValue(mcpTools)
|
||||
|
|
@ -788,7 +783,7 @@ describe('MultipleToolSelector', () => {
|
|||
]
|
||||
|
||||
// Act
|
||||
renderComponent({ value: tools, canChooseMCPTool: false })
|
||||
renderComponent({ value: tools, versionSupported: false })
|
||||
|
||||
// Assert - Only regular tool should be counted
|
||||
expect(screen.getByText('1/2')).toBeInTheDocument()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Divider from '@/app/components/base/divider'
|
|||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import { useAllMCPTools } from '@/service/use-tools'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
|
|
@ -27,7 +28,6 @@ type Props = {
|
|||
nodeOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
nodeId?: string
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const MultipleToolSelector = ({
|
||||
|
|
@ -42,14 +42,14 @@ const MultipleToolSelector = ({
|
|||
nodeOutputVars,
|
||||
availableNodes,
|
||||
nodeId,
|
||||
canChooseMCPTool,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const enabledCount = value.filter((item) => {
|
||||
const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name)
|
||||
if (isMCPTool)
|
||||
return item.enabled && canChooseMCPTool
|
||||
return item.enabled && isMCPToolAllowed
|
||||
return item.enabled
|
||||
}).length
|
||||
// collapse control
|
||||
|
|
@ -167,7 +167,6 @@ const MultipleToolSelector = ({
|
|||
onSelectMultiple={handleAddMultiple}
|
||||
onDelete={() => handleDelete(index)}
|
||||
supportEnableSwitch
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isEdit
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -190,7 +189,6 @@ const MultipleToolSelector = ({
|
|||
panelShowState={panelShowState}
|
||||
onPanelShowStateChange={setPanelShowState}
|
||||
isEdit={false}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onSelectMultiple={handleAddMultiple}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ type Props = {
|
|||
nodeOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
nodeId?: string
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
const ToolSelector: FC<Props> = ({
|
||||
value,
|
||||
|
|
@ -86,7 +85,6 @@ const ToolSelector: FC<Props> = ({
|
|||
nodeOutputVars,
|
||||
availableNodes,
|
||||
nodeId = '',
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShow, onShowChange] = useState(false)
|
||||
|
|
@ -267,7 +265,6 @@ const ToolSelector: FC<Props> = ({
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
@ -300,7 +297,6 @@ const ToolSelector: FC<Props> = ({
|
|||
onSelectMultiple={handleSelectMultipleTool}
|
||||
scope={scope}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
|||
import { ToolTipContent } from '@/app/components/base/tooltip/content'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -39,7 +40,6 @@ type Props = {
|
|||
versionMismatch?: boolean
|
||||
open: boolean
|
||||
authRemoved?: boolean
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolItem = ({
|
||||
|
|
@ -61,13 +61,13 @@ const ToolItem = ({
|
|||
errorTip,
|
||||
versionMismatch,
|
||||
authRemoved,
|
||||
canChooseMCPTool,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
|
||||
const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop()
|
||||
const isTransparent = uninstalled || versionMismatch || isError
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool
|
||||
const isShowCanNotChooseMCPTip = isMCPTool && !isMCPToolAllowed
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
|
|
@ -125,9 +125,7 @@ const ToolItem = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{isShowCanNotChooseMCPTip && (
|
||||
<McpToolNotSupportTooltip />
|
||||
)}
|
||||
{isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />}
|
||||
{!isError && !uninstalled && !versionMismatch && noAuth && (
|
||||
<Button variant="secondary" size="small">
|
||||
{t('notAuthorized', { ns: 'tools' })}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ type AllToolsProps = {
|
|||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
onTagsChange?: Dispatch<SetStateAction<string[]>>
|
||||
isInRAGPipeline?: boolean
|
||||
featuredPlugins?: Plugin[]
|
||||
|
|
@ -71,7 +70,6 @@ const AllTools = ({
|
|||
customTools,
|
||||
mcpTools = [],
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
onTagsChange,
|
||||
isInRAGPipeline = false,
|
||||
featuredPlugins = [],
|
||||
|
|
@ -249,7 +247,6 @@ const AllTools = ({
|
|||
providerMap={providerMap}
|
||||
onSelect={onSelect}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isLoading={featuredLoading}
|
||||
onInstallSuccess={async () => {
|
||||
await onFeaturedInstallSuccess?.()
|
||||
|
|
@ -275,7 +272,6 @@ const AllTools = ({
|
|||
viewType={isSupportGroupView ? activeView : ViewType.flat}
|
||||
hasSearchText={hasSearchText}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ type FeaturedToolsProps = {
|
|||
providerMap: Map<string, ToolWithProvider>
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
isLoading?: boolean
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
|
|
@ -42,7 +41,6 @@ const FeaturedTools = ({
|
|||
providerMap,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
isLoading = false,
|
||||
onInstallSuccess,
|
||||
}: FeaturedToolsProps) => {
|
||||
|
|
@ -166,7 +164,6 @@ const FeaturedTools = ({
|
|||
viewType={ViewType.flat}
|
||||
hasSearchText={false}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,6 @@ const Tabs: FC<TabsProps> = ({
|
|||
customTools={customTools || []}
|
||||
workflowTools={workflowTools || []}
|
||||
mcpTools={mcpTools || []}
|
||||
canChooseMCPTool
|
||||
onTagsChange={onTagsChange}
|
||||
isInRAGPipeline={inRAGPipeline}
|
||||
featuredPlugins={featuredPlugins}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ type Props = {
|
|||
supportAddCustomTool?: boolean
|
||||
scope?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
|
|
@ -66,7 +65,6 @@ const ToolPicker: FC<Props> = ({
|
|||
scope = 'all',
|
||||
selectedTools,
|
||||
panelClassName,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
|
@ -198,7 +196,6 @@ const ToolPicker: FC<Props> = ({
|
|||
workflowTools={workflowToolList || []}
|
||||
mcpTools={mcpTools || []}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onTagsChange={setTags}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ type Props = {
|
|||
letters: string[]
|
||||
toolRefs: any
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolViewFlatView: FC<Props> = ({
|
||||
|
|
@ -32,7 +31,6 @@ const ToolViewFlatView: FC<Props> = ({
|
|||
onSelectMultiple,
|
||||
toolRefs,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const firstLetterToolIds = useMemo(() => {
|
||||
const res: Record<string, string> = {}
|
||||
|
|
@ -63,7 +61,6 @@ const ToolViewFlatView: FC<Props> = ({
|
|||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ type Props = {
|
|||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
|
|
@ -25,7 +24,6 @@ const Item: FC<Props> = ({
|
|||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -43,7 +41,6 @@ const Item: FC<Props> = ({
|
|||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ type Props = {
|
|||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolListTreeView: FC<Props> = ({
|
||||
|
|
@ -25,7 +24,6 @@ const ToolListTreeView: FC<Props> = ({
|
|||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const getI18nGroupName = useCallback((name: string) => {
|
||||
|
|
@ -56,7 +54,6 @@ const ToolListTreeView: FC<Props> = ({
|
|||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import * as React from 'react'
|
|||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
|
|
@ -38,7 +39,6 @@ type Props = {
|
|||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
isShowLetterIndex?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +51,9 @@ const Tool: FC<Props> = ({
|
|||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
|
||||
const language = useGetLanguage()
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
|
|
@ -63,7 +63,7 @@ const Tool: FC<Props> = ({
|
|||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
const isMCPTool = payload.type === CollectionType.mcp
|
||||
const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool
|
||||
const isShowCanNotChooseMCPTip = !isMCPToolAllowed && isMCPTool
|
||||
const { theme } = useTheme()
|
||||
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(payload.icon) ?? payload.icon
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ type ToolsProps = {
|
|||
className?: string
|
||||
indexBarClassName?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
const Tools = ({
|
||||
onSelect,
|
||||
|
|
@ -35,7 +34,6 @@ const Tools = ({
|
|||
className,
|
||||
indexBarClassName,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}: ToolsProps) => {
|
||||
// const tools: any = []
|
||||
const language = useGetLanguage()
|
||||
|
|
@ -109,7 +107,6 @@ const Tools = ({
|
|||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
/>
|
||||
)
|
||||
|
|
@ -121,7 +118,6 @@ const Tools = ({
|
|||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -92,13 +92,12 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
|
|||
export type AgentStrategySelectorProps = {
|
||||
value?: Strategy
|
||||
onChange: (value?: Strategy) => void
|
||||
canChooseMCPTool: boolean
|
||||
}
|
||||
|
||||
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const { value, onChange, canChooseMCPTool } = props
|
||||
const { value, onChange } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
|
||||
const [query, setQuery] = useState('')
|
||||
|
|
@ -242,7 +241,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
|
|||
indexBarClassName="top-0 xl:top-36"
|
||||
hasSearchText={false}
|
||||
canNotSelectMultiple
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isAgent
|
||||
/>
|
||||
{enable_marketplace && (
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ export type AgentStrategyProps = {
|
|||
nodeOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
nodeId?: string
|
||||
canChooseMCPTool: boolean
|
||||
}
|
||||
|
||||
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
|
||||
|
|
@ -54,7 +53,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
|
|||
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
|
||||
|
||||
export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props
|
||||
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
|
|
@ -189,7 +188,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
value={value}
|
||||
onSelect={item => onChange(item)}
|
||||
onDelete={() => onChange(null)}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onSelectMultiple={noop}
|
||||
/>
|
||||
</Field>
|
||||
|
|
@ -212,7 +210,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
onChange={onChange}
|
||||
supportCollapse
|
||||
required={schema.required}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -220,7 +217,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
}
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} />
|
||||
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
|
||||
{
|
||||
strategy
|
||||
? (
|
||||
|
|
@ -241,7 +238,6 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
|||
nodeId={nodeId}
|
||||
nodeOutputVars={nodeOutputVars || []}
|
||||
availableNodes={availableNodes || []}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
type MCPToolAvailabilityContextValue = {
|
||||
versionSupported?: boolean
|
||||
}
|
||||
|
||||
const MCPToolAvailabilityContext = createContext<MCPToolAvailabilityContextValue | undefined>(undefined)
|
||||
|
||||
export type MCPToolAvailability = {
|
||||
allowed: boolean
|
||||
versionSupported?: boolean
|
||||
}
|
||||
|
||||
export const MCPToolAvailabilityProvider = ({
|
||||
versionSupported,
|
||||
children,
|
||||
}: {
|
||||
versionSupported?: boolean
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<MCPToolAvailabilityContext.Provider value={{ versionSupported }}>
|
||||
{children}
|
||||
</MCPToolAvailabilityContext.Provider>
|
||||
)
|
||||
|
||||
export const useMCPToolAvailability = (): MCPToolAvailability => {
|
||||
const context = useContext(MCPToolAvailabilityContext)
|
||||
if (context === undefined)
|
||||
return { allowed: true }
|
||||
|
||||
const { versionSupported } = context
|
||||
return {
|
||||
allowed: versionSupported === true,
|
||||
versionSupported,
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,11 @@ import type { StrategyParamItem } from '@/app/components/plugins/types'
|
|||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toType } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { isSupportMCP } from '@/utils/plugin-version-feature'
|
||||
import { useStore } from '../../store'
|
||||
import { AgentStrategy } from '../_base/components/agent-strategy'
|
||||
import Field from '../_base/components/field'
|
||||
import { MCPToolAvailabilityProvider } from '../_base/components/mcp-tool-availability'
|
||||
import MemoryConfig from '../_base/components/memory-config'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import Split from '../_base/components/split'
|
||||
|
|
@ -40,9 +42,9 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
|||
readOnly,
|
||||
outputSchema,
|
||||
handleMemoryChange,
|
||||
canChooseMCPTool,
|
||||
} = useConfig(props.id, props.data)
|
||||
const { t } = useTranslation()
|
||||
const isMCPVersionSupported = isSupportMCP(inputs.meta?.version)
|
||||
|
||||
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
return (
|
||||
|
|
@ -53,37 +55,38 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
|||
className="px-4 py-2"
|
||||
tooltip={t('nodes.agent.strategy.tooltip', { ns: 'workflow' })}
|
||||
>
|
||||
<AgentStrategy
|
||||
strategy={inputs.agent_strategy_name
|
||||
? {
|
||||
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
|
||||
agent_strategy_name: inputs.agent_strategy_name!,
|
||||
agent_strategy_label: inputs.agent_strategy_label!,
|
||||
agent_output_schema: inputs.output_schema,
|
||||
plugin_unique_identifier: inputs.plugin_unique_identifier!,
|
||||
meta: inputs.meta,
|
||||
}
|
||||
: undefined}
|
||||
onStrategyChange={(strategy) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
|
||||
agent_strategy_name: strategy?.agent_strategy_name,
|
||||
agent_strategy_label: strategy?.agent_strategy_label,
|
||||
output_schema: strategy!.agent_output_schema,
|
||||
plugin_unique_identifier: strategy!.plugin_unique_identifier,
|
||||
meta: strategy?.meta,
|
||||
})
|
||||
resetEditor(Date.now())
|
||||
}}
|
||||
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
|
||||
formValue={formData}
|
||||
onFormValueChange={onFormChange}
|
||||
nodeOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
nodeId={props.id}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
<MCPToolAvailabilityProvider versionSupported={isMCPVersionSupported}>
|
||||
<AgentStrategy
|
||||
strategy={inputs.agent_strategy_name
|
||||
? {
|
||||
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
|
||||
agent_strategy_name: inputs.agent_strategy_name!,
|
||||
agent_strategy_label: inputs.agent_strategy_label!,
|
||||
agent_output_schema: inputs.output_schema,
|
||||
plugin_unique_identifier: inputs.plugin_unique_identifier!,
|
||||
meta: inputs.meta,
|
||||
}
|
||||
: undefined}
|
||||
onStrategyChange={(strategy) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
|
||||
agent_strategy_name: strategy?.agent_strategy_name,
|
||||
agent_strategy_label: strategy?.agent_strategy_label,
|
||||
output_schema: strategy!.agent_output_schema,
|
||||
plugin_unique_identifier: strategy!.plugin_unique_identifier,
|
||||
meta: strategy?.meta,
|
||||
})
|
||||
resetEditor(Date.now())
|
||||
}}
|
||||
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
|
||||
formValue={formData}
|
||||
onFormValueChange={onFormChange}
|
||||
nodeOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
nodeId={props.id}
|
||||
/>
|
||||
</MCPToolAvailabilityProvider>
|
||||
</Field>
|
||||
<div className="px-4 py-2">
|
||||
{isChatMode && currentStrategy?.features?.includes(AgentFeature.HISTORY_MESSAGES) && (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '@/app/components/workflow/hooks'
|
||||
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
|
||||
import { useStrategyProviderDetail } from '@/service/use-strategy'
|
||||
import { isSupportMCP } from '@/utils/plugin-version-feature'
|
||||
import { VarType as VarKindType } from '../../types'
|
||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
|
|
@ -222,7 +221,6 @@ const useConfig = (id: string, payload: AgentNodeType) => {
|
|||
outputSchema,
|
||||
handleMemoryChange,
|
||||
isChatMode,
|
||||
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,10 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
|||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
// Track login success event
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
if (res?.data?.access_token) {
|
||||
// Track login success event
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
}
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_password',
|
||||
is_invite: isInvite,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const offlineUrl = `${basePath}/_offline.html`
|
|||
const serwist = new Serwist({
|
||||
precacheEntries: self.__SW_MANIFEST,
|
||||
skipWaiting: true,
|
||||
disableDevLogs: true,
|
||||
clientsClaim: true,
|
||||
navigationPreload: true,
|
||||
runtimeCaching: [
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import { del, get, patch, post, put } from './base'
|
|||
|
||||
type LoginSuccess = {
|
||||
result: 'success'
|
||||
data: { access_token: string }
|
||||
data?: { access_token?: string }
|
||||
}
|
||||
type LoginFail = {
|
||||
result: 'fail'
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { createApp, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
|
||||
import {
|
||||
useAppDailyConversations,
|
||||
useAppDailyEndUsers,
|
||||
useAppDetail,
|
||||
useAppList,
|
||||
} from '../use-apps'
|
||||
|
||||
const Service: FC = () => {
|
||||
const appId = '1'
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: appList, error: appListError, isLoading: isAppListLoading } = useAppList({ page: 1, limit: 30, name: '' })
|
||||
const { data: firstApp, error: appDetailError, isLoading: isAppDetailLoading } = useAppDetail(appId)
|
||||
|
||||
const { data: updateAppSiteStatusRes, error: err1, isLoading: isUpdatingSiteStatus } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteStatus', appId],
|
||||
queryFn: () => updateAppSiteStatus({ url: '/apps', body: { enable_site: false } }),
|
||||
})
|
||||
const { data: updateAppApiStatusRes, error: err2, isLoading: isUpdatingApiStatus } = useQuery({
|
||||
queryKey: ['demo', 'updateAppApiStatus', appId],
|
||||
queryFn: () => updateAppApiStatus({ url: '/apps', body: { enable_api: true } }),
|
||||
})
|
||||
const { data: updateAppRateLimitRes, error: err3, isLoading: isUpdatingRateLimit } = useQuery({
|
||||
queryKey: ['demo', 'updateAppRateLimit', appId],
|
||||
queryFn: () => updateAppRateLimit({ url: '/apps', body: { api_rpm: 10, api_rph: 20 } }),
|
||||
})
|
||||
const { data: updateAppSiteCodeRes, error: err4, isLoading: isUpdatingSiteCode } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteAccessToken', appId],
|
||||
queryFn: () => updateAppSiteAccessToken({ url: '/apps' }),
|
||||
})
|
||||
const { data: updateAppSiteConfigRes, error: err5, isLoading: isUpdatingSiteConfig } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteConfig', appId],
|
||||
queryFn: () => updateAppSiteConfig({ url: '/apps', body: { title: 'title test', author: 'author test' } }),
|
||||
})
|
||||
|
||||
const { data: getAppDailyConversationsRes, error: err6, isLoading: isConversationsLoading } = useAppDailyConversations(appId, { start: '1', end: '2' })
|
||||
const { data: getAppDailyEndUsersRes, error: err7, isLoading: isEndUsersLoading } = useAppDailyEndUsers(appId, { start: '1', end: '2' })
|
||||
|
||||
const { data: updateAppModelConfigRes, error: err8, isLoading: isUpdatingModelConfig } = useQuery({
|
||||
queryKey: ['demo', 'updateAppModelConfig', appId],
|
||||
queryFn: () => updateAppModelConfig({ url: '/apps', body: { model_id: 'gpt-100' } }),
|
||||
})
|
||||
|
||||
const { mutateAsync: mutateCreateApp } = useMutation({
|
||||
mutationKey: ['demo', 'createApp'],
|
||||
mutationFn: () => createApp({
|
||||
name: `new app${Math.round(Math.random() * 100)}`,
|
||||
mode: AppModeEnum.CHAT,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['apps', 'list'],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleCreateApp = async () => {
|
||||
await mutateCreateApp()
|
||||
}
|
||||
|
||||
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
|
||||
return <div>{JSON.stringify(appListError ?? appDetailError ?? err1 ?? err2 ?? err3 ?? err4 ?? err5 ?? err6 ?? err7 ?? err8)}</div>
|
||||
|
||||
const isLoading = isAppListLoading
|
||||
|| isAppDetailLoading
|
||||
|| isUpdatingSiteStatus
|
||||
|| isUpdatingApiStatus
|
||||
|| isUpdatingRateLimit
|
||||
|| isUpdatingSiteCode
|
||||
|| isUpdatingSiteConfig
|
||||
|| isConversationsLoading
|
||||
|| isEndUsersLoading
|
||||
|| isUpdatingModelConfig
|
||||
|
||||
if (isLoading || !appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div>1.App list</div>
|
||||
<div>
|
||||
{appList.data.map(item => (
|
||||
<div key={item.id}>
|
||||
{item.id}
|
||||
{' '}
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>2.First app detail</div>
|
||||
<div>{JSON.stringify(firstApp)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" onClick={handleCreateApp}>Click me to Create App</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>4.updateAppSiteStatusRes</div>
|
||||
<div>{JSON.stringify(updateAppSiteStatusRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>5.updateAppApiStatusRes</div>
|
||||
<div>{JSON.stringify(updateAppApiStatusRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>6.updateAppRateLimitRes</div>
|
||||
<div>{JSON.stringify(updateAppRateLimitRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>7.updateAppSiteCodeRes</div>
|
||||
<div>{JSON.stringify(updateAppSiteCodeRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>8.updateAppSiteConfigRes</div>
|
||||
<div>{JSON.stringify(updateAppSiteConfigRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>9.getAppDailyConversationsRes</div>
|
||||
<div>{JSON.stringify(getAppDailyConversationsRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>10.getAppDailyEndUsersRes</div>
|
||||
<div>{JSON.stringify(getAppDailyEndUsersRes)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>11.updateAppModelConfigRes</div>
|
||||
<div>{JSON.stringify(updateAppModelConfigRes)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Service)
|
||||
|
|
@ -127,8 +127,25 @@ export const getBaseOptions = (): RequestInit => ({
|
|||
})
|
||||
|
||||
async function base<T>(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise<T> {
|
||||
const baseOptions = getBaseOptions()
|
||||
const { params, body, headers, ...init } = Object.assign({}, baseOptions, options)
|
||||
// In fetchCompat mode, skip baseOptions to avoid overriding Request object's method, headers,
|
||||
const baseOptions = otherOptions.fetchCompat
|
||||
? {
|
||||
mode: 'cors',
|
||||
credentials: 'include', // always send cookies、HTTP Basic authentication.
|
||||
redirect: 'follow',
|
||||
}
|
||||
: {
|
||||
mode: 'cors',
|
||||
credentials: 'include', // always send cookies、HTTP Basic authentication.
|
||||
headers: new Headers({
|
||||
'Content-Type': ContentType.json,
|
||||
}),
|
||||
method: 'GET',
|
||||
redirect: 'follow',
|
||||
}
|
||||
const { params, body, headers: headersFromProps, ...init } = Object.assign({}, baseOptions, options)
|
||||
const headers = new Headers(headersFromProps || {})
|
||||
|
||||
const {
|
||||
isPublicAPI = false,
|
||||
isMarketplaceAPI = false,
|
||||
|
|
@ -156,14 +173,14 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||
|
||||
const fetchPathname = base + (url.startsWith('/') ? url : `/${url}`)
|
||||
if (!isMarketplaceAPI)
|
||||
(headers as any).set(CSRF_HEADER_NAME, Cookies.get(CSRF_COOKIE_NAME()) || '')
|
||||
headers.set(CSRF_HEADER_NAME, Cookies.get(CSRF_COOKIE_NAME()) || '')
|
||||
|
||||
if (deleteContentType)
|
||||
(headers as any).delete('Content-Type')
|
||||
headers.delete('Content-Type')
|
||||
|
||||
// ! For Marketplace API, help to filter tags added in new version
|
||||
if (isMarketplaceAPI)
|
||||
(headers as any).set('X-Dify-Version', !IS_MARKETPLACE ? APP_VERSION : '999.0.0')
|
||||
headers.set('X-Dify-Version', !IS_MARKETPLACE ? APP_VERSION : '999.0.0')
|
||||
|
||||
const client = baseClient.extend({
|
||||
hooks: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue