Compare commits

...

10 Commits

Author SHA1 Message Date
HyaCinth
09b0dd5583
fix: Optimize scrolling experience on plugin page (#24314) (#24322)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
2025-08-22 16:09:10 +08:00
Eric Guo
455f842785
Flask 3.1.2 upgrade fix by Avoids using current_user in background thread (#24290)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-08-22 14:47:13 +08:00
AADI GUPTA
6b01b0b165
feat: implement TooltipManager for managing tooltip lifecycle (#24236)
Co-authored-by: crazywoola <427733928@qq.com>
2025-08-22 10:42:48 +08:00
Asuka Minato
c5614d04d2
an example of sessionmaker (#24246) 2025-08-22 10:17:50 +08:00
Asuka Minato
1459fded08
Annotations example (#24304) 2025-08-22 10:14:17 +08:00
Yongtao Huang
6b466a8469
[Test] add unit tests for web_reader_tool.py (#24309)
Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-08-22 09:28:00 +08:00
NeatGuyCoding
21c56c3107
feature: add test containers base tests for tag service (#24313) 2025-08-22 09:27:51 +08:00
willzhao
5ab6bc283c
[CHORE]: x: T = None to x: Optional[T] = None (#24217) 2025-08-21 21:58:39 +08:00
Yongtao Huang
106ab7f2a8
Fix: safe defaults for BaseModel dict fields (#24098)
Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-08-21 21:38:55 +08:00
Charles Lee
0c595c4745
style: replace h-[1px] with h-px to unify the writing format of Tailwind CSS (#24146) 2025-08-21 21:38:40 +08:00
68 changed files with 1613 additions and 131 deletions

View File

@ -1,4 +1,4 @@
from typing import Any
from typing import Any, Optional
import flask_restful
from flask_login import current_user
@ -49,7 +49,7 @@ class BaseApiKeyListResource(Resource):
method_decorators = [account_initialization_required, login_required, setup_required]
resource_type: str | None = None
resource_model: Any = None
resource_model: Optional[Any] = None
resource_id_field: str | None = None
token_prefix: str | None = None
max_keys = 10
@ -102,7 +102,7 @@ class BaseApiKeyResource(Resource):
method_decorators = [account_initialization_required, login_required, setup_required]
resource_type: str | None = None
resource_model: Any = None
resource_model: Optional[Any] = None
resource_id_field: str | None = None
def delete(self, resource_id, api_key_id):

View File

@ -167,7 +167,7 @@ class ModelConfig(BaseModel):
provider: str
name: str
mode: LLMMode
completion_params: dict[str, Any] = {}
completion_params: dict[str, Any] = Field(default_factory=dict)
class Condition(BaseModel):

View File

@ -610,7 +610,7 @@ class QueueErrorEvent(AppQueueEvent):
"""
event: QueueEvent = QueueEvent.ERROR
error: Any = None
error: Optional[Any] = None
class QueuePingEvent(AppQueueEvent):

View File

@ -142,7 +142,7 @@ class MessageEndStreamResponse(StreamResponse):
event: StreamEvent = StreamEvent.MESSAGE_END
id: str
metadata: dict = {}
metadata: dict = Field(default_factory=dict)
files: Optional[Sequence[Mapping[str, Any]]] = None
@ -261,7 +261,7 @@ class NodeStartStreamResponse(StreamResponse):
predecessor_node_id: Optional[str] = None
inputs: Optional[Mapping[str, Any]] = None
created_at: int
extras: dict = {}
extras: dict = Field(default_factory=dict)
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
parent_parallel_id: Optional[str] = None
@ -503,7 +503,7 @@ class IterationNodeStartStreamResponse(StreamResponse):
node_type: str
title: str
created_at: int
extras: dict = {}
extras: dict = Field(default_factory=dict)
metadata: Mapping = {}
inputs: Mapping = {}
parallel_id: Optional[str] = None
@ -531,7 +531,7 @@ class IterationNodeNextStreamResponse(StreamResponse):
index: int
created_at: int
pre_iteration_output: Optional[Any] = None
extras: dict = {}
extras: dict = Field(default_factory=dict)
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
parallel_mode_run_id: Optional[str] = None
@ -590,7 +590,7 @@ class LoopNodeStartStreamResponse(StreamResponse):
node_type: str
title: str
created_at: int
extras: dict = {}
extras: dict = Field(default_factory=dict)
metadata: Mapping = {}
inputs: Mapping = {}
parallel_id: Optional[str] = None
@ -618,7 +618,7 @@ class LoopNodeNextStreamResponse(StreamResponse):
index: int
created_at: int
pre_loop_output: Optional[Any] = None
extras: dict = {}
extras: dict = Field(default_factory=dict)
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
parallel_mode_run_id: Optional[str] = None
@ -764,7 +764,7 @@ class ChatbotAppBlockingResponse(AppBlockingResponse):
conversation_id: str
message_id: str
answer: str
metadata: dict = {}
metadata: dict = Field(default_factory=dict)
created_at: int
data: Data
@ -784,7 +784,7 @@ class CompletionAppBlockingResponse(AppBlockingResponse):
mode: str
message_id: str
answer: str
metadata: dict = {}
metadata: dict = Field(default_factory=dict)
created_at: int
data: Data

View File

@ -52,7 +52,8 @@ class BasedGenerateTaskPipeline:
elif isinstance(e, InvokeError | ValueError):
err = e
else:
err = Exception(e.description if getattr(e, "description", None) is not None else str(e))
description = getattr(e, "description", None)
err = Exception(description if description is not None else str(e))
if not message_id or not session:
return err

View File

@ -17,7 +17,7 @@ class ExtensionModule(enum.Enum):
class ModuleExtension(BaseModel):
extension_class: Any = None
extension_class: Optional[Any] = None
name: str
label: Optional[dict] = None
form_schema: Optional[list] = None

View File

@ -38,6 +38,7 @@ class Extension:
def extension_class(self, module: ExtensionModule, extension_name: str) -> type:
module_extension = self.module_extension(module, extension_name)
assert module_extension.extension_class is not None
t: type = module_extension.extension_class
return t

View File

@ -9,7 +9,6 @@ import uuid
from typing import Any, Optional, cast
from flask import current_app
from flask_login import current_user
from sqlalchemy.orm.exc import ObjectDeletedError
from configs import dify_config
@ -295,7 +294,7 @@ class IndexingRunner:
text_docs,
embedding_model_instance=embedding_model_instance,
process_rule=processing_rule.to_dict(),
tenant_id=current_user.current_tenant_id,
tenant_id=tenant_id,
doc_language=doc_language,
preview=True,
)

View File

@ -4,7 +4,7 @@ from collections.abc import Callable
from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError
from datetime import timedelta
from types import TracebackType
from typing import Any, Generic, Self, TypeVar
from typing import Any, Generic, Optional, Self, TypeVar
from httpx import HTTPStatusError
from pydantic import BaseModel
@ -209,7 +209,7 @@ class BaseSession(
request: SendRequestT,
result_type: type[ReceiveResultT],
request_read_timeout_seconds: timedelta | None = None,
metadata: MessageMetadata = None,
metadata: Optional[MessageMetadata] = None,
) -> ReceiveResultT:
"""
Sends a request and wait for a response. Raises an McpError if the

View File

@ -1173,7 +1173,7 @@ class SessionMessage:
"""A message with specific metadata for transport-specific features."""
message: JSONRPCMessage
metadata: MessageMetadata = None
metadata: Optional[MessageMetadata] = None
class OAuthClientMetadata(BaseModel):

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from decimal import Decimal
from enum import StrEnum
@ -54,7 +56,7 @@ class LLMUsage(ModelUsage):
)
@classmethod
def from_metadata(cls, metadata: dict) -> "LLMUsage":
def from_metadata(cls, metadata: dict) -> LLMUsage:
"""
Create LLMUsage instance from metadata dictionary with default values.
@ -84,7 +86,7 @@ class LLMUsage(ModelUsage):
latency=metadata.get("latency", 0.0),
)
def plus(self, other: "LLMUsage") -> "LLMUsage":
def plus(self, other: LLMUsage) -> LLMUsage:
"""
Add two LLMUsage instances together.
@ -109,7 +111,7 @@ class LLMUsage(ModelUsage):
latency=self.latency + other.latency,
)
def __add__(self, other: "LLMUsage") -> "LLMUsage":
def __add__(self, other: LLMUsage) -> LLMUsage:
"""
Overload the + operator to add two LLMUsage instances.

View File

@ -1,10 +1,10 @@
import logging
from threading import Lock
from typing import Any
from typing import Any, Optional
logger = logging.getLogger(__name__)
_tokenizer: Any = None
_tokenizer: Optional[Any] = None
_lock = Lock()

View File

@ -1,6 +1,6 @@
from typing import Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
from core.extension.api_based_extension_requestor import APIBasedExtensionPoint, APIBasedExtensionRequestor
from core.helper.encrypter import decrypt_token
@ -11,7 +11,7 @@ from models.api_based_extension import APIBasedExtension
class ModerationInputParams(BaseModel):
app_id: str = ""
inputs: dict = {}
inputs: dict = Field(default_factory=dict)
query: str = ""

View File

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
from core.extension.extensible import Extensible, ExtensionModule
@ -16,7 +16,7 @@ class ModerationInputsResult(BaseModel):
flagged: bool = False
action: ModerationAction
preset_response: str = ""
inputs: dict = {}
inputs: dict = Field(default_factory=dict)
query: str = ""

View File

@ -1,6 +1,6 @@
from collections.abc import Generator
from datetime import datetime
from typing import Any
from typing import Any, Optional
from core.rag.extractor.watercrawl.client import WaterCrawlAPIClient
@ -9,7 +9,7 @@ class WaterCrawlProvider:
def __init__(self, api_key, base_url: str | None = None):
self.client = WaterCrawlAPIClient(api_key, base_url)
def crawl_url(self, url, options: dict | Any = None) -> dict:
def crawl_url(self, url, options: Optional[dict | Any] = None) -> dict:
options = options or {}
spider_options = {
"max_depth": 1,

View File

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import Any, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
class ChildDocument(BaseModel):
@ -15,7 +15,7 @@ class ChildDocument(BaseModel):
"""Arbitrary metadata about the page content (e.g., source, relationships to other
documents, etc.).
"""
metadata: dict = {}
metadata: dict = Field(default_factory=dict)
class Document(BaseModel):
@ -28,7 +28,7 @@ class Document(BaseModel):
"""Arbitrary metadata about the page content (e.g., source, relationships to other
documents, etc.).
"""
metadata: dict = {}
metadata: dict = Field(default_factory=dict)
provider: Optional[str] = "dify"

View File

@ -80,7 +80,7 @@ def get_url(url: str, user_agent: Optional[str] = None) -> str:
else:
content = response.text
article = extract_using_readability(content)
article = extract_using_readabilipy(content)
if not article.text:
return ""
@ -101,7 +101,7 @@ class Article:
text: Sequence[dict]
def extract_using_readability(html: str):
def extract_using_readabilipy(html: str):
json_article: dict[str, Any] = simple_json_from_html_string(html, use_readability=True)
article = Article(
title=json_article.get("title") or "",

View File

@ -22,7 +22,7 @@ class GraphRuntimeState(BaseModel):
#
# Note: Since the type of this field is `dict[str, Any]`, its values may not remain consistent
# after a serialization and deserialization round trip.
outputs: dict[str, Any] = {}
outputs: dict[str, Any] = Field(default_factory=dict)
node_run_steps: int = 0
"""node run steps"""

View File

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast
from sqlalchemy import Float, and_, func, or_, text
from sqlalchemy import cast as sqlalchemy_cast
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from core.app.app_config.entities import DatasetRetrieveConfigEntity
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
@ -175,7 +175,7 @@ class KnowledgeRetrievalNode(BaseNode):
redis_client.zremrangebyscore(key, 0, current_time - 60000)
request_count = redis_client.zcard(key)
if request_count > knowledge_rate_limit.limit:
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
# add ratelimit record
rate_limit_log = RateLimitLog(
tenant_id=self.tenant_id,
@ -183,7 +183,6 @@ class KnowledgeRetrievalNode(BaseNode):
operation="knowledge",
)
session.add(rate_limit_log)
session.commit()
return NodeRunResult(
status=WorkflowNodeExecutionStatus.FAILED,
inputs=variables,

View File

@ -13,7 +13,7 @@ class ModelConfig(BaseModel):
provider: str
name: str
mode: LLMMode
completion_params: dict[str, Any] = {}
completion_params: dict[str, Any] = Field(default_factory=dict)
class ContextConfig(BaseModel):

View File

@ -3,7 +3,7 @@ import logging
import ssl
from collections.abc import Callable
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Union
from typing import TYPE_CHECKING, Any, Optional, Union
import redis
from redis import RedisError
@ -246,7 +246,7 @@ def init_app(app: DifyApp):
app.extensions["redis"] = redis_client
def redis_fallback(default_return: Any = None):
def redis_fallback(default_return: Optional[Any] = None):
"""
decorator to handle Redis operation exceptions and return a default value when Redis is unavailable.

View File

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any, Optional, Union
from uuid import uuid4
import sqlalchemy as sa
from flask_login import current_user
from sqlalchemy import DateTime, orm
from core.file.constants import maybe_file_object
@ -18,7 +17,6 @@ from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIAB
from core.workflow.nodes.enums import NodeType
from factories.variable_factory import TypeMismatchError, build_segment_with_type
from libs.datetime_utils import naive_utc_now
from libs.helper import extract_tenant_id
from ._workflow_exc import NodeNotFoundError, WorkflowDataError
@ -351,8 +349,8 @@ class Workflow(Base):
if self._environment_variables is None:
self._environment_variables = "{}"
# Get tenant_id from current_user (Account or EndUser)
tenant_id = extract_tenant_id(current_user)
# Use workflow.tenant_id to avoid relying on request user in background threads
tenant_id = self.tenant_id
if not tenant_id:
return []
@ -382,8 +380,8 @@ class Workflow(Base):
self._environment_variables = "{}"
return
# Get tenant_id from current_user (Account or EndUser)
tenant_id = extract_tenant_id(current_user)
# Use workflow.tenant_id to avoid relying on request user in background threads
tenant_id = self.tenant_id
if not tenant_id:
self._environment_variables = "{}"

View File

@ -13,7 +13,7 @@ dependencies = [
"cachetools~=5.3.0",
"celery~=5.5.2",
"chardet~=5.1.0",
"flask~=3.1.0",
"flask~=3.1.2",
"flask-compress~=1.17",
"flask-cors~=6.0.0",
"flask-login~=0.6.3",

View File

@ -1,4 +1,5 @@
import logging
from typing import Optional
from core.tools.entities.api_entities import ToolProviderTypeApiLiteral
from core.tools.tool_manager import ToolManager
@ -9,7 +10,7 @@ logger = logging.getLogger(__name__)
class ToolCommonService:
@staticmethod
def list_tool_providers(user_id: str, tenant_id: str, typ: ToolProviderTypeApiLiteral = None):
def list_tool_providers(user_id: str, tenant_id: str, typ: Optional[ToolProviderTypeApiLiteral] = None):
"""
list tool providers

View File

@ -402,7 +402,7 @@ class WorkflowConverter:
)
role_prefix = None
prompts: Any = None
prompts: Optional[Any] = None
# Chat Model
if model_config.mode == LLMMode.CHAT.value:

View File

@ -1,5 +1,6 @@
import os
from collections import UserDict
from typing import Optional
from unittest.mock import MagicMock
import pytest
@ -21,7 +22,7 @@ class MockBaiduVectorDBClass:
def mock_vector_db_client(
self,
config=None,
adapter: HTTPAdapter = None,
adapter: Optional[HTTPAdapter] = None,
):
self.conn = MagicMock()
self._config = MagicMock()

View File

@ -23,7 +23,7 @@ class MockTcvectordbClass:
key="",
read_consistency: ReadConsistency = ReadConsistency.EVENTUAL_CONSISTENCY,
timeout=10,
adapter: HTTPAdapter = None,
adapter: Optional[HTTPAdapter] = None,
pool_size: int = 2,
proxies: Optional[dict] = None,
password: Optional[str] = None,
@ -72,11 +72,11 @@ class MockTcvectordbClass:
shard: int,
replicas: int,
description: Optional[str] = None,
index: Index = None,
embedding: Embedding = None,
index: Optional[Index] = None,
embedding: Optional[Embedding] = None,
timeout: Optional[float] = None,
ttl_config: Optional[dict] = None,
filter_index_config: FilterIndexConfig = None,
filter_index_config: Optional[FilterIndexConfig] = None,
indexes: Optional[list[IndexField]] = None,
) -> RPCCollection:
return RPCCollection(
@ -113,7 +113,7 @@ class MockTcvectordbClass:
database_name: str,
collection_name: str,
vectors: list[list[float]],
filter: Filter = None,
filter: Optional[Filter] = None,
params=None,
retrieve_vector: bool = False,
limit: int = 10,
@ -128,7 +128,7 @@ class MockTcvectordbClass:
collection_name: str,
ann: Optional[Union[list[AnnSearch], AnnSearch]] = None,
match: Optional[Union[list[KeywordSearch], KeywordSearch]] = None,
filter: Union[Filter, str] = None,
filter: Optional[Union[Filter, str]] = None,
rerank: Optional[Rerank] = None,
retrieve_vector: Optional[bool] = None,
output_fields: Optional[list[str]] = None,
@ -158,7 +158,7 @@ class MockTcvectordbClass:
database_name: str,
collection_name: str,
document_ids: Optional[list[str]] = None,
filter: Filter = None,
filter: Optional[Filter] = None,
timeout: Optional[float] = None,
):
return {"code": 0, "msg": "operation success"}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,293 @@
from core.tools.utils.web_reader_tool import get_image_upload_file_ids
import pytest
from core.tools.utils.web_reader_tool import (
extract_using_readabilipy,
get_image_upload_file_ids,
get_url,
page_result,
)
class FakeResponse:
"""Minimal fake response object for ssrf_proxy / cloudscraper."""
def __init__(self, *, status_code=200, headers=None, content=b"", text=""):
self.status_code = status_code
self.headers = headers or {}
self.content = content
self.text = text if text else content.decode("utf-8", errors="ignore")
# ---------------------------
# Tests: page_result
# ---------------------------
@pytest.mark.parametrize(
("text", "cursor", "maxlen", "expected"),
[
("abcdef", 0, 3, "abc"),
("abcdef", 2, 10, "cdef"), # maxlen beyond end
("abcdef", 6, 5, ""), # cursor at end
("abcdef", 7, 5, ""), # cursor beyond end
("", 0, 5, ""), # empty text
],
)
def test_page_result(text, cursor, maxlen, expected):
assert page_result(text, cursor, maxlen) == expected
# ---------------------------
# Tests: get_url
# ---------------------------
@pytest.fixture
def stub_support_types(monkeypatch):
"""Stub supported content types list."""
import core.tools.utils.web_reader_tool as mod
# e.g. binary types supported by ExtractProcessor
monkeypatch.setattr(mod.extract_processor, "SUPPORT_URL_CONTENT_TYPES", ["application/pdf", "text/plain"])
return mod
def test_get_url_unsupported_content_type(monkeypatch, stub_support_types):
# HEAD 200 but content-type not supported and not text/html
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(
status_code=200,
headers={"Content-Type": "image/png"}, # not supported
)
monkeypatch.setattr(stub_support_types.ssrf_proxy, "head", fake_head)
result = get_url("https://x.test/file.png")
assert result == "Unsupported content-type [image/png] of URL."
def test_get_url_supported_binary_type_uses_extract_processor(monkeypatch, stub_support_types):
"""
When content-type is in SUPPORT_URL_CONTENT_TYPES,
should call ExtractProcessor.load_from_url and return its text.
"""
calls = {"load": 0}
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(
status_code=200,
headers={"Content-Type": "application/pdf"},
)
def fake_load_from_url(url, return_text=False):
calls["load"] += 1
assert return_text is True
return "PDF extracted text"
monkeypatch.setattr(stub_support_types.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(stub_support_types.ExtractProcessor, "load_from_url", staticmethod(fake_load_from_url))
result = get_url("https://x.test/doc.pdf")
assert calls["load"] == 1
assert result == "PDF extracted text"
def test_get_url_html_flow_with_chardet_and_readability(monkeypatch, stub_support_types):
"""200 + text/html → GET, chardet detects encoding, readability returns article which is templated."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"})
def fake_get(url, headers=None, follow_redirects=True, timeout=None):
html = b"<html><head><title>x</title></head><body>hello</body></html>"
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"}, content=html)
# chardet.detect returns utf-8
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(mod.ssrf_proxy, "get", fake_get)
monkeypatch.setattr(mod.chardet, "detect", lambda b: {"encoding": "utf-8"})
# readability → a dict that maps to Article, then FULL_TEMPLATE
def fake_simple_json_from_html_string(html, use_readability=True):
return {
"title": "My Title",
"byline": "Bob",
"plain_text": [{"type": "text", "text": "Hello world"}],
}
monkeypatch.setattr(mod, "simple_json_from_html_string", fake_simple_json_from_html_string)
out = get_url("https://x.test/page")
assert "TITLE: My Title" in out
assert "AUTHOR: Bob" in out
assert "Hello world" in out
def test_get_url_html_flow_empty_article_text_returns_empty(monkeypatch, stub_support_types):
"""If readability returns no text, should return empty string."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"})
def fake_get(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"}, content=b"<html/>")
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(mod.ssrf_proxy, "get", fake_get)
monkeypatch.setattr(mod.chardet, "detect", lambda b: {"encoding": "utf-8"})
# readability returns empty plain_text
monkeypatch.setattr(mod, "simple_json_from_html_string", lambda html, use_readability=True: {"plain_text": []})
out = get_url("https://x.test/empty")
assert out == ""
def test_get_url_403_cloudscraper_fallback(monkeypatch, stub_support_types):
"""HEAD 403 → use cloudscraper.get via ssrf_proxy.make_request, then proceed."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=403, headers={})
# cloudscraper.create_scraper() → object with .get()
class FakeScraper:
def __init__(self):
pass # removed unused attribute
def get(self, url, headers=None, follow_redirects=True, timeout=None):
# mimic html 200
html = b"<html><body>hi</body></html>"
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"}, content=html)
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(mod.cloudscraper, "create_scraper", lambda: FakeScraper())
monkeypatch.setattr(mod.chardet, "detect", lambda b: {"encoding": "utf-8"})
monkeypatch.setattr(
mod,
"simple_json_from_html_string",
lambda html, use_readability=True: {"title": "T", "byline": "A", "plain_text": [{"type": "text", "text": "X"}]},
)
out = get_url("https://x.test/403")
assert "TITLE: T" in out
assert "AUTHOR: A" in out
assert "X" in out
def test_get_url_head_non_200_returns_status(monkeypatch, stub_support_types):
"""HEAD returns non-200 and non-403 → should directly return code message."""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=500)
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
out = get_url("https://x.test/fail")
assert out == "URL returned status code 500."
def test_get_url_content_disposition_filename_detection(monkeypatch, stub_support_types):
"""
If HEAD 200 with no Content-Type but Content-Disposition filename suggests a supported type,
it should route to ExtractProcessor.load_from_url.
"""
calls = {"load": 0}
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=200, headers={"Content-Disposition": 'attachment; filename="doc.pdf"'})
def fake_load_from_url(url, return_text=False):
calls["load"] += 1
return "From ExtractProcessor via filename"
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(mod.ExtractProcessor, "load_from_url", staticmethod(fake_load_from_url))
out = get_url("https://x.test/fname")
assert calls["load"] == 1
assert out == "From ExtractProcessor via filename"
def test_get_url_html_encoding_fallback_when_decode_fails(monkeypatch, stub_support_types):
"""
If chardet returns an encoding but content.decode raises, should fallback to response.text.
"""
def fake_head(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(status_code=200, headers={"Content-Type": "text/html"})
# Return bytes that will raise with the chosen encoding
def fake_get(url, headers=None, follow_redirects=True, timeout=None):
return FakeResponse(
status_code=200,
headers={"Content-Type": "text/html"},
content=b"\xff\xfe\xfa", # likely to fail under utf-8
text="<html>fallback text</html>",
)
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod.ssrf_proxy, "head", fake_head)
monkeypatch.setattr(mod.ssrf_proxy, "get", fake_get)
monkeypatch.setattr(mod.chardet, "detect", lambda b: {"encoding": "utf-8"})
monkeypatch.setattr(
mod,
"simple_json_from_html_string",
lambda html, use_readability=True: {"title": "", "byline": "", "plain_text": [{"type": "text", "text": "ok"}]},
)
out = get_url("https://x.test/enc-fallback")
assert "ok" in out
# ---------------------------
# Tests: extract_using_readabilipy
# ---------------------------
def test_extract_using_readabilipy_field_mapping_and_defaults(monkeypatch):
# stub readabilipy.simple_json_from_html_string
def fake_simple_json_from_html_string(html, use_readability=True):
return {
"title": "Hello",
"byline": "Alice",
"plain_text": [{"type": "text", "text": "world"}],
}
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod, "simple_json_from_html_string", fake_simple_json_from_html_string)
article = extract_using_readabilipy("<html>...</html>")
assert article.title == "Hello"
assert article.author == "Alice"
assert isinstance(article.text, list)
assert article.text
assert article.text[0]["text"] == "world"
def test_extract_using_readabilipy_defaults_when_missing(monkeypatch):
def fake_simple_json_from_html_string(html, use_readability=True):
return {} # all missing
import core.tools.utils.web_reader_tool as mod
monkeypatch.setattr(mod, "simple_json_from_html_string", fake_simple_json_from_html_string)
article = extract_using_readabilipy("<html>...</html>")
assert article.title == ""
assert article.author == ""
assert article.text == []
# ---------------------------
# Tests: get_image_upload_file_ids
# ---------------------------
def test_get_image_upload_file_ids():
# should extract id from https + file-preview
content = "![image](https://example.com/a/b/files/abc123/file-preview)"

View File

@ -9,7 +9,6 @@ from core.file.models import File
from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable
from core.variables.segments import IntegerSegment, Segment
from factories.variable_factory import build_segment
from models.model import EndUser
from models.workflow import Workflow, WorkflowDraftVariable, WorkflowNodeExecutionModel, is_system_variable_editable
@ -43,14 +42,9 @@ def test_environment_variables():
{"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]}
)
# Mock current_user as an EndUser
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
):
# Set the environment_variables property of the Workflow instance
variables = [variable1, variable2, variable3, variable4]
@ -90,14 +84,9 @@ def test_update_environment_variables():
{"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]}
)
# Mock current_user as an EndUser
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
):
variables = [variable1, variable2, variable3, variable4]
@ -136,14 +125,9 @@ def test_to_dict():
# Create some EnvironmentVariable instances
# Mock current_user as an EndUser
mock_user = mock.Mock(spec=EndUser)
mock_user.tenant_id = "tenant_id"
with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
):
# Set the environment_variables property of the Workflow instance
workflow.environment_variables = [

8
api/uv.lock generated
View File

@ -1436,7 +1436,7 @@ requires-dist = [
{ name = "cachetools", specifier = "~=5.3.0" },
{ name = "celery", specifier = "~=5.5.2" },
{ name = "chardet", specifier = "~=5.1.0" },
{ name = "flask", specifier = "~=3.1.0" },
{ name = "flask", specifier = "~=3.1.2" },
{ name = "flask-compress", specifier = "~=1.17" },
{ name = "flask-cors", specifier = "~=6.0.0" },
{ name = "flask-login", specifier = "~=0.6.3" },
@ -1790,7 +1790,7 @@ wheels = [
[[package]]
name = "flask"
version = "3.1.1"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@ -1800,9 +1800,9 @@ dependencies = [
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]

View File

@ -107,7 +107,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
)}
</div>
<div className='px-4'>
<div className={cn('mx-auto mt-1 h-[1px] bg-divider-subtle', !expand && 'w-6')} />
<div className={cn('mx-auto mt-1 h-px bg-divider-subtle', !expand && 'w-6')} />
</div>
<nav
className={`

View File

@ -24,7 +24,7 @@ export const EditTitle: FC<{ className?: string; title: string }> = ({ className
<RiEditFill className='mr-1 h-3.5 w-3.5' />
<div>{title}</div>
<div
className='ml-2 h-[1px] grow'
className='ml-2 h-px grow'
style={{
background: 'linear-gradient(90deg, rgba(0, 0, 0, 0.05) -1.65%, rgba(0, 0, 0, 0.00) 100%)',
}}

View File

@ -12,7 +12,7 @@ const GroupName: FC<IGroupNameProps> = ({
return (
<div className='mb-1 flex items-center'>
<div className='mr-3 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{name}</div>
<div className='h-[1px] grow'
<div className='h-px grow'
style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',

View File

@ -66,7 +66,7 @@ const SelectVarType: FC<Props> = ({
<SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConfig.select')} onClick={handleChange}></SelectItem>
<SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConfig.number')} onClick={handleChange}></SelectItem>
</div>
<div className='h-[1px] border-t border-components-panel-border'></div>
<div className='h-px border-t border-components-panel-border'></div>
<div className='p-1'>
<SelectItem Icon={ApiConnection} value='api' text={t('appDebug.variableConfig.apiBasedVar')} onClick={handleChange}></SelectItem>
</div>

View File

@ -81,7 +81,7 @@ const AssistantTypePicker: FC<Props> = ({
const agentConfigUI = (
<>
<div className='my-4 h-[1px] bg-gray-100'></div>
<div className='my-4 h-px bg-gray-100'></div>
<div
className={cn(isAgent ? 'group cursor-pointer hover:bg-primary-50' : 'opacity-30', 'rounded-xl bg-gray-50 p-3 pr-4 ')}
onClick={() => {

View File

@ -678,7 +678,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
? <div className="px-6 py-4">
<div className='flex h-[18px] items-center space-x-3'>
<div className='system-xs-semibold-uppercase text-text-tertiary'>{t('appLog.table.header.output')}</div>
<div className='h-[1px] grow' style={{
<div className='h-px grow' style={{
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
}}></div>
</div>

View File

@ -24,7 +24,7 @@ const Iteration: FC<Props> = ({ iterationInfo, isFinal, index }) => {
{!isFinal && (
<div className='mr-3 shrink-0 text-xs font-semibold leading-[18px] text-text-tertiary'>{`${t('appLog.agentLogDetail.iteration').toUpperCase()} ${index}`}</div>
)}
<Divider bgStyle='gradient' className='mx-0 h-[1px] grow'/>
<Divider bgStyle='gradient' className='mx-0 h-px grow'/>
</div>
<ToolCall
isLLM

View File

@ -79,7 +79,7 @@ const Citation: FC<CitationProps> = ({
<div className='-mb-1 mt-3'>
<div className='system-xs-medium mb-2 flex items-center text-text-tertiary'>
{t('common.chat.citation.title')}
<div className='ml-2 h-[1px] grow bg-divider-regular' />
<div className='ml-2 h-px grow bg-divider-regular' />
</div>
<div className='relative flex flex-wrap'>
{

View File

@ -114,7 +114,7 @@ const Popup: FC<PopupProps> = ({
</div>
{
index !== data.sources.length - 1 && (
<div className='my-1 h-[1px] bg-divider-regular' />
<div className='my-1 h-px bg-divider-regular' />
)
}
</Fragment>

View File

@ -77,7 +77,7 @@ const Dropdown: FC<DropdownProps> = ({
}
{
(!!items.length && !!secondItems?.length) && (
<div className='h-[1px] bg-divider-regular' />
<div className='h-px bg-divider-regular' />
)
}
{

View File

@ -19,7 +19,7 @@ const ScoreSlider: FC<Props> = ({
return (
<div className={className}>
<div className='mt-[14px] h-[1px]'>
<div className='mt-[14px] h-px'>
<Slider
max={100}
min={80}

View File

@ -101,9 +101,9 @@ const FileFromLinkOrLocal = ({
{
showFromLink && showFromLocal && (
<div className='system-2xs-medium-uppercase flex h-7 items-center p-2 text-text-quaternary'>
<div className='mr-2 h-[1px] w-[93px] bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
<div className='mr-2 h-px w-[93px] bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
OR
<div className='ml-2 h-[1px] w-[93px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
<div className='ml-2 h-px w-[93px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</div>
)
}

View File

@ -93,9 +93,9 @@ const UploaderButton: FC<UploaderButtonProps> = ({
{hasUploadFromLocal && (
<>
<div className="mt-2 flex items-center px-2 text-xs font-medium text-gray-400">
<div className="mr-3 h-[1px] w-[93px] bg-gradient-to-l from-[#F3F4F6]" />
<div className="mr-3 h-px w-[93px] bg-gradient-to-l from-[#F3F4F6]" />
OR
<div className="ml-3 h-[1px] w-[93px] bg-gradient-to-r from-[#F3F4F6]" />
<div className="ml-3 h-px w-[93px] bg-gradient-to-r from-[#F3F4F6]" />
</div>
<Uploader
onUpload={handleUpload}

View File

@ -150,7 +150,7 @@ const Panel = (props: PanelProps) => {
</div>
)}
{keywords && notExisted && filteredTagList.length > 0 && (
<Divider className='!my-0 !h-[1px]' />
<Divider className='!my-0 !h-px' />
)}
{(filteredTagList.length > 0 || filteredSelectedTagList.length > 0) && (
<div className='max-h-[172px] overflow-y-auto p-1'>
@ -192,7 +192,7 @@ const Panel = (props: PanelProps) => {
</div>
</div>
)}
<Divider className='!my-0 !h-[1px]' />
<Divider className='!my-0 !h-px' />
<div className='p-1'>
<div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => setShowTagManagementModal(true)}>
<Tag03 className='h-4 w-4 text-text-tertiary' />

View File

@ -0,0 +1,16 @@
class TooltipManager {
private activeCloser: (() => void) | null = null
register(closeFn: () => void) {
if (this.activeCloser)
this.activeCloser()
this.activeCloser = closeFn
}
clear(closeFn: () => void) {
if (this.activeCloser === closeFn)
this.activeCloser = null
}
}
export const tooltipManager = new TooltipManager()

View File

@ -6,6 +6,8 @@ import type { OffsetOptions, Placement } from '@floating-ui/react'
import { RiQuestionLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { tooltipManager } from './TooltipManager'
export type TooltipProps = {
position?: Placement
triggerMethod?: 'hover' | 'click'
@ -56,22 +58,26 @@ const Tooltip: FC<TooltipProps> = ({
isHoverTriggerRef.current = isHoverTrigger
}, [isHoverTrigger])
const close = () => setOpen(false)
const handleLeave = (isTrigger: boolean) => {
if (isTrigger)
setNotHoverTrigger()
else
setNotHoverPopup()
// give time to move to the popup
if (needsDelay) {
setTimeout(() => {
if (!isHoverPopupRef.current && !isHoverTriggerRef.current)
if (!isHoverPopupRef.current && !isHoverTriggerRef.current) {
setOpen(false)
tooltipManager.clear(close)
}
}, 300)
}
else {
setOpen(false)
tooltipManager.clear(close)
}
}
@ -87,6 +93,7 @@ const Tooltip: FC<TooltipProps> = ({
onMouseEnter={() => {
if (triggerMethod === 'hover') {
setHoverTrigger()
tooltipManager.register(close)
setOpen(true)
}
}}

View File

@ -101,7 +101,6 @@ const RuleDetail: FC<{
break
}
return value
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sourceData])
return <div className='flex flex-col gap-1'>
@ -196,7 +195,6 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
return () => {
stopQueryStatus()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// get rule
@ -334,7 +332,7 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
</div>
))}
</div>
<hr className="my-3 h-[1px] border-0 bg-divider-subtle" />
<hr className="my-3 h-px border-0 bg-divider-subtle" />
<RuleDetail
sourceData={ruleDetail}
indexingType={indexingType}

View File

@ -39,7 +39,7 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
</div>
</div>
</div>
<hr className="my-6 h-[1px] border-0 bg-divider-subtle" />
<hr className="my-6 h-px border-0 bg-divider-subtle" />
</>
)}
{datasetId && (

View File

@ -93,7 +93,7 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
isParagraphMode ? 'pb-2 pt-1' : 'grow px-3',
(isFullDocMode && isLoading) && 'overflow-y-hidden',
)}>
{isFullDocMode ? <Divider type='horizontal' className='my-1 h-[1px] bg-divider-subtle' /> : null}
{isFullDocMode ? <Divider type='horizontal' className='my-1 h-px bg-divider-subtle' /> : null}
<div className={classNames('flex items-center justify-between', isFullDocMode ? 'sticky -top-2 left-0 bg-background-default pb-3 pt-2' : '')}>
<div className={classNames(
'flex h-7 items-center rounded-lg pl-1 pr-3',

View File

@ -175,7 +175,6 @@ const Completed: FC<ICompletedProps> = ({
if (totalPages < currentPage)
setCurrentPage(totalPages === 0 ? 1 : totalPages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentListData])
useEffect(() => {
@ -214,7 +213,6 @@ const Completed: FC<ICompletedProps> = ({
if (totalPages < currentPage)
setCurrentPage(totalPages === 0 ? 1 : totalPages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childChunkListData])
const resetList = useCallback(() => {
@ -375,13 +373,11 @@ const Completed: FC<ICompletedProps> = ({
useEffect(() => {
resetList()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
useEffect(() => {
if (importStatus === ProcessStatus.COMPLETED)
resetList()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importStatus])
const onCancelBatchOperation = useCallback(() => {
@ -655,7 +651,7 @@ const Completed: FC<ICompletedProps> = ({
/>
}
{/* Pagination */}
<Divider type='horizontal' className='mx-6 my-0 h-[1px] w-auto bg-divider-subtle' />
<Divider type='horizontal' className='mx-6 my-0 h-px w-auto bg-divider-subtle' />
<Pagination
current={currentPage - 1}
onChange={cur => setCurrentPage(cur + 1)}

View File

@ -105,7 +105,7 @@ const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
}
</div>
</div>
<div className='h-[1px] bg-divider-regular' />
<div className='h-px bg-divider-regular' />
<div className='p-1'>
<div
className='flex h-8 cursor-pointer items-center px-3 text-sm text-text-accent'

View File

@ -206,7 +206,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
</div>
{
!!parameterRules.length && (
<div className='my-3 h-[1px] bg-divider-subtle' />
<div className='my-3 h-px bg-divider-subtle' />
)
}
{

View File

@ -14,7 +14,7 @@ const IntersectionLine = ({
useScrollIntersection(ref, intersectionContainerId)
return (
<div ref={ref} className='mb-4 h-[1px] shrink-0 bg-transparent'></div>
<div ref={ref} className='mb-4 h-px shrink-0 bg-transparent'></div>
)
}

View File

@ -294,7 +294,7 @@ const Authorized = ({
)
}
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='h-px bg-divider-subtle'></div>
<div className='p-2'>
<Authorize
pluginPayload={pluginPayload}

View File

@ -248,7 +248,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
/>
</div>
{(currentModel?.model_type === ModelTypeEnum.textGeneration || currentModel?.model_type === ModelTypeEnum.tts) && (
<div className='my-3 h-[1px] bg-divider-subtle' />
<div className='my-3 h-px bg-divider-subtle' />
)}
{currentModel?.model_type === ModelTypeEnum.textGeneration && (
<LLMParamsPanel

View File

@ -73,7 +73,7 @@ const PluginsPanel = () => {
{!isPluginListLoading && (
<>
{(filteredList?.length ?? 0) > 0 ? (
<div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'>
<div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch overflow-y-auto px-12'>
<div className='w-full'>
<List pluginList={filteredList || []} />
</div>

View File

@ -175,7 +175,7 @@ const WorkflowToolConfigureButton = ({
return (
<>
<Divider type='horizontal' className='h-[1px] bg-divider-subtle' />
<Divider type='horizontal' className='h-px bg-divider-subtle' />
{(!published || !isLoading) && (
<div className={cn(
'group rounded-lg bg-background-section-burn transition-colors',

View File

@ -15,7 +15,7 @@ const HelpLineHorizontal = memo(({
return (
<div
className='absolute z-[9] h-[1px] bg-primary-300'
className='absolute z-[9] h-px bg-primary-300'
style={{
top: top * zoom + y,
left: left * zoom + x,

View File

@ -119,7 +119,7 @@ const PanelOperatorPopup = ({
)
}
</div>
<div className='h-[1px] bg-divider-regular'></div>
<div className='h-px bg-divider-regular'></div>
</>
)
}
@ -148,7 +148,7 @@ const PanelOperatorPopup = ({
<ShortcutsName keys={['ctrl', 'd']} />
</div>
</div>
<div className='h-[1px] bg-divider-regular'></div>
<div className='h-px bg-divider-regular'></div>
<div className='p-1'>
<div
className={`
@ -161,7 +161,7 @@ const PanelOperatorPopup = ({
<ShortcutsName keys={['del']} />
</div>
</div>
<div className='h-[1px] bg-divider-regular'></div>
<div className='h-px bg-divider-regular'></div>
</>
)
}
@ -177,7 +177,7 @@ const PanelOperatorPopup = ({
{t('workflow.panel.helpLink')}
</a>
</div>
<div className='h-[1px] bg-divider-regular'></div>
<div className='h-px bg-divider-regular'></div>
</>
)
}

View File

@ -204,7 +204,7 @@ const ConditionWrap: FC<Props> = ({
</div>
</div>
{!isSubVariable && (
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
<div className='mx-3 my-2 h-px bg-divider-subtle'></div>
)}
</div>
))

View File

@ -73,7 +73,7 @@ const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
ELIF
</Button>
</div>
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
<div className='mx-3 my-2 h-px bg-divider-subtle'></div>
<Field
title={t(`${i18nPrefix}.else`)}
className='px-4 py-2'

View File

@ -71,7 +71,7 @@ const Operator = ({
<ShortcutsName keys={['ctrl', 'd']} />
</div>
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='h-px bg-divider-subtle'></div>
<div className='p-1'>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-md px-3 text-sm text-text-secondary hover:bg-state-base-hover'
@ -85,7 +85,7 @@ const Operator = ({
/>
</div>
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='h-px bg-divider-subtle'></div>
<div className='p-1'>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-md px-3 text-sm text-text-secondary hover:bg-state-destructive-hover hover:text-text-destructive'

View File

@ -107,7 +107,7 @@ const ConversationVariableModal = ({
<div className='flex h-0 grow flex-col p-4 pt-2'>
<div className='mb-2 flex shrink-0 items-center gap-2'>
<div className='system-xs-medium-uppercase shrink-0 text-text-tertiary'>{t('workflow.chatVariable.storedContent').toLocaleUpperCase()}</div>
<div className='h-[1px] grow' style={{
<div className='h-px grow' style={{
background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
}}></div>
{latestValueTimestampMap[currentVar.id] && (

View File

@ -64,7 +64,7 @@ const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
{
isShowDelete && (
<>
<Divider type='horizontal' className='my-0 h-[1px] bg-divider-subtle' />
<Divider type='horizontal' className='my-0 h-px bg-divider-subtle' />
<div className='p-1'>
<MenuItem
item={deleteOperation}

View File

@ -70,7 +70,7 @@ const Filter: FC<FilterProps> = ({
})
}
</div>
<Divider type='horizontal' className='my-0 h-[1px] bg-divider-subtle' />
<Divider type='horizontal' className='my-0 h-px bg-divider-subtle' />
<FilterSwitch enabled={isOnlyShowNamedVersions} handleSwitch={handleSwitch} />
</div>
</PortalToFollowElemContent>

View File

@ -414,7 +414,7 @@ const SelectionContextmenu = () => {
{t('workflow.operator.distributeVertical')}
</div>
</div>
<div className='h-[1px] bg-divider-regular'></div>
<div className='h-px bg-divider-regular'></div>
<div className='p-1'>
<div className='system-xs-medium px-2 py-2 text-text-tertiary'>
{t('workflow.operator.horizontal')}

View File

@ -154,7 +154,7 @@ const EducationApplyAge = () => {
>
{t('education.submit')}
</Button>
<div className='mb-4 mt-5 h-[1px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]'></div>
<div className='mb-4 mt-5 h-px bg-gradient-to-r from-[rgba(16,24,40,0.08)]'></div>
<a
className='system-xs-regular flex items-center text-text-accent'
href={docLink('/getting-started/dify-for-education')}