diff --git a/api/core/rag/extractor/firecrawl/firecrawl_app.py b/api/core/rag/extractor/firecrawl/firecrawl_app.py
index c20ecd2b89..789ac8557d 100644
--- a/api/core/rag/extractor/firecrawl/firecrawl_app.py
+++ b/api/core/rag/extractor/firecrawl/firecrawl_app.py
@@ -25,7 +25,7 @@ class FirecrawlApp:
}
if params:
json_data.update(params)
- response = self._post_request(f"{self.base_url}/v1/scrape", json_data, headers)
+ response = self._post_request(f"{self.base_url}/v2/scrape", json_data, headers)
if response.status_code == 200:
response_data = response.json()
data = response_data["data"]
@@ -42,7 +42,7 @@ class FirecrawlApp:
json_data = {"url": url}
if params:
json_data.update(params)
- response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers)
+ response = self._post_request(f"{self.base_url}/v2/crawl", json_data, headers)
if response.status_code == 200:
# There's also another two fields in the response: "success" (bool) and "url" (str)
job_id = response.json().get("id")
@@ -51,9 +51,25 @@ class FirecrawlApp:
self._handle_error(response, "start crawl job")
return "" # unreachable
+ def map(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
+ # Documentation: https://docs.firecrawl.dev/api-reference/endpoint/map
+ headers = self._prepare_headers()
+ json_data: dict[str, Any] = {"url": url, "integration": "dify"}
+ if params:
+ # Pass through provided params, including optional "sitemap": "only" | "include" | "skip"
+ json_data.update(params)
+ response = self._post_request(f"{self.base_url}/v2/map", json_data, headers)
+ if response.status_code == 200:
+ return cast(dict[str, Any], response.json())
+ elif response.status_code in {402, 409, 500, 429, 408}:
+ self._handle_error(response, "start map job")
+ return {}
+ else:
+ raise Exception(f"Failed to start map job. Status code: {response.status_code}")
+
def check_crawl_status(self, job_id) -> dict[str, Any]:
headers = self._prepare_headers()
- response = self._get_request(f"{self.base_url}/v1/crawl/{job_id}", headers)
+ response = self._get_request(f"{self.base_url}/v2/crawl/{job_id}", headers)
if response.status_code == 200:
crawl_status_response = response.json()
if crawl_status_response.get("status") == "completed":
@@ -135,12 +151,16 @@ class FirecrawlApp:
"lang": "en",
"country": "us",
"timeout": 60000,
- "ignoreInvalidURLs": False,
+ "ignoreInvalidURLs": True,
"scrapeOptions": {},
+ "sources": [
+ {"type": "web"},
+ ],
+ "integration": "dify",
}
if params:
json_data.update(params)
- response = self._post_request(f"{self.base_url}/v1/search", json_data, headers)
+ response = self._post_request(f"{self.base_url}/v2/search", json_data, headers)
if response.status_code == 200:
response_data = response.json()
if not response_data.get("success"):
diff --git a/api/core/workflow/graph_engine/command_channels/redis_channel.py b/api/core/workflow/graph_engine/command_channels/redis_channel.py
index c841459170..527647ae3b 100644
--- a/api/core/workflow/graph_engine/command_channels/redis_channel.py
+++ b/api/core/workflow/graph_engine/command_channels/redis_channel.py
@@ -41,6 +41,7 @@ class RedisChannel:
self._redis = redis_client
self._key = channel_key
self._command_ttl = command_ttl
+ self._pending_key = f"{channel_key}:pending"
def fetch_commands(self) -> list[GraphEngineCommand]:
"""
@@ -49,6 +50,9 @@ class RedisChannel:
Returns:
List of pending commands (drains the Redis list)
"""
+ if not self._has_pending_commands():
+ return []
+
commands: list[GraphEngineCommand] = []
# Use pipeline for atomic operations
@@ -85,6 +89,7 @@ class RedisChannel:
with self._redis.pipeline() as pipe:
pipe.rpush(self._key, command_json)
pipe.expire(self._key, self._command_ttl)
+ pipe.set(self._pending_key, "1", ex=self._command_ttl)
pipe.execute()
def _deserialize_command(self, data: dict[str, Any]) -> GraphEngineCommand | None:
@@ -112,3 +117,17 @@ class RedisChannel:
except (ValueError, TypeError):
return None
+
+ def _has_pending_commands(self) -> bool:
+ """
+ Check and consume the pending marker to avoid unnecessary list reads.
+
+ Returns:
+ True if commands should be fetched from Redis.
+ """
+ with self._redis.pipeline() as pipe:
+ pipe.get(self._pending_key)
+ pipe.delete(self._pending_key)
+ pending_value, _ = pipe.execute()
+
+ return pending_value is not None
diff --git a/api/core/workflow/graph_engine/event_management/event_handlers.py b/api/core/workflow/graph_engine/event_management/event_handlers.py
index 7247b17967..1cb5851ab1 100644
--- a/api/core/workflow/graph_engine/event_management/event_handlers.py
+++ b/api/core/workflow/graph_engine/event_management/event_handlers.py
@@ -7,6 +7,7 @@ from collections.abc import Mapping
from functools import singledispatchmethod
from typing import TYPE_CHECKING, final
+from core.model_runtime.entities.llm_entities import LLMUsage
from core.workflow.entities import GraphRuntimeState
from core.workflow.enums import ErrorStrategy, NodeExecutionType
from core.workflow.graph import Graph
@@ -125,6 +126,7 @@ class EventHandler:
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
is_initial_attempt = node_execution.retry_count == 0
node_execution.mark_started(event.id)
+ self._graph_runtime_state.increment_node_run_steps()
# Track in response coordinator for stream ordering
self._response_coordinator.track_node_execution(event.node_id, event.id)
@@ -163,6 +165,8 @@ class EventHandler:
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
node_execution.mark_taken()
+ self._accumulate_node_usage(event.node_run_result.llm_usage)
+
# Store outputs in variable pool
self._store_node_outputs(event.node_id, event.node_run_result.outputs)
@@ -212,6 +216,8 @@ class EventHandler:
node_execution.mark_failed(event.error)
self._graph_execution.record_node_failure()
+ self._accumulate_node_usage(event.node_run_result.llm_usage)
+
result = self._error_handler.handle_node_failure(event)
if result:
@@ -235,6 +241,8 @@ class EventHandler:
node_execution = self._graph_execution.get_or_create_node_execution(event.node_id)
node_execution.mark_taken()
+ self._accumulate_node_usage(event.node_run_result.llm_usage)
+
# Persist outputs produced by the exception strategy (e.g. default values)
self._store_node_outputs(event.node_id, event.node_run_result.outputs)
@@ -286,6 +294,19 @@ class EventHandler:
self._state_manager.enqueue_node(event.node_id)
self._state_manager.start_execution(event.node_id)
+ def _accumulate_node_usage(self, usage: LLMUsage) -> None:
+ """Accumulate token usage into the shared runtime state."""
+ if usage.total_tokens <= 0:
+ return
+
+ self._graph_runtime_state.add_tokens(usage.total_tokens)
+
+ current_usage = self._graph_runtime_state.llm_usage
+ if current_usage.total_tokens == 0:
+ self._graph_runtime_state.llm_usage = usage
+ else:
+ self._graph_runtime_state.llm_usage = current_usage.plus(usage)
+
def _store_node_outputs(self, node_id: str, outputs: Mapping[str, object]) -> None:
"""
Store node outputs in the variable pool.
diff --git a/api/core/workflow/graph_engine/orchestration/dispatcher.py b/api/core/workflow/graph_engine/orchestration/dispatcher.py
index a7229ce4e8..8340c10b49 100644
--- a/api/core/workflow/graph_engine/orchestration/dispatcher.py
+++ b/api/core/workflow/graph_engine/orchestration/dispatcher.py
@@ -8,7 +8,12 @@ import threading
import time
from typing import TYPE_CHECKING, final
-from core.workflow.graph_events.base import GraphNodeEventBase
+from core.workflow.graph_events import (
+ GraphNodeEventBase,
+ NodeRunExceptionEvent,
+ NodeRunFailedEvent,
+ NodeRunSucceededEvent,
+)
from ..event_management import EventManager
from .execution_coordinator import ExecutionCoordinator
@@ -72,13 +77,16 @@ class Dispatcher:
if self._thread and self._thread.is_alive():
self._thread.join(timeout=10.0)
+ _COMMAND_TRIGGER_EVENTS = (
+ NodeRunSucceededEvent,
+ NodeRunFailedEvent,
+ NodeRunExceptionEvent,
+ )
+
def _dispatcher_loop(self) -> None:
"""Main dispatcher loop."""
try:
while not self._stop_event.is_set():
- # Check for commands
- self._execution_coordinator.check_commands()
-
# Check for scaling
self._execution_coordinator.check_scaling()
@@ -87,6 +95,8 @@ class Dispatcher:
event = self._event_queue.get(timeout=0.1)
# Route to the event handler
self._event_handler.dispatch(event)
+ if self._should_check_commands(event):
+ self._execution_coordinator.check_commands()
self._event_queue.task_done()
except queue.Empty:
# Check if execution is complete
@@ -102,3 +112,7 @@ class Dispatcher:
# Signal the event emitter that execution is complete
if self._event_emitter:
self._event_emitter.mark_complete()
+
+ def _should_check_commands(self, event: GraphNodeEventBase) -> bool:
+ """Return True if the event represents a node completion."""
+ return isinstance(event, self._COMMAND_TRIGGER_EVENTS)
diff --git a/api/services/website_service.py b/api/services/website_service.py
index 37588d6ba5..a23f01ec71 100644
--- a/api/services/website_service.py
+++ b/api/services/website_service.py
@@ -23,6 +23,7 @@ class CrawlOptions:
only_main_content: bool = False
includes: str | None = None
excludes: str | None = None
+ prompt: str | None = None
max_depth: int | None = None
use_sitemap: bool = True
@@ -70,6 +71,7 @@ class WebsiteCrawlApiRequest:
only_main_content=self.options.get("only_main_content", False),
includes=self.options.get("includes"),
excludes=self.options.get("excludes"),
+ prompt=self.options.get("prompt"),
max_depth=self.options.get("max_depth"),
use_sitemap=self.options.get("use_sitemap", True),
)
@@ -174,6 +176,7 @@ class WebsiteService:
def _crawl_with_firecrawl(cls, request: CrawlRequest, api_key: str, config: dict) -> dict[str, Any]:
firecrawl_app = FirecrawlApp(api_key=api_key, base_url=config.get("base_url"))
+ params: dict[str, Any]
if not request.options.crawl_sub_pages:
params = {
"includePaths": [],
@@ -188,8 +191,10 @@ class WebsiteService:
"limit": request.options.limit,
"scrapeOptions": {"onlyMainContent": request.options.only_main_content},
}
- if request.options.max_depth:
- params["maxDepth"] = request.options.max_depth
+
+ # Add optional prompt for Firecrawl v2 crawl-params compatibility
+ if request.options.prompt:
+ params["prompt"] = request.options.prompt
job_id = firecrawl_app.crawl_url(request.url, params)
website_crawl_time_cache_key = f"website_crawl_{job_id}"
diff --git a/api/tests/unit_tests/core/workflow/graph_engine/command_channels/test_redis_channel.py b/api/tests/unit_tests/core/workflow/graph_engine/command_channels/test_redis_channel.py
index 7ebccf83a7..8677325d4e 100644
--- a/api/tests/unit_tests/core/workflow/graph_engine/command_channels/test_redis_channel.py
+++ b/api/tests/unit_tests/core/workflow/graph_engine/command_channels/test_redis_channel.py
@@ -35,11 +35,15 @@ class TestRedisChannel:
"""Test sending a command to Redis."""
mock_redis = MagicMock()
mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ context = MagicMock()
+ context.__enter__.return_value = mock_pipe
+ context.__exit__.return_value = None
+ mock_redis.pipeline.return_value = context
channel = RedisChannel(mock_redis, "test:key", 3600)
+ pending_key = "test:key:pending"
+
# Create a test command
command = GraphEngineCommand(command_type=CommandType.ABORT)
@@ -55,6 +59,7 @@ class TestRedisChannel:
# Verify expire was set
mock_pipe.expire.assert_called_once_with("test:key", 3600)
+ mock_pipe.set.assert_called_once_with(pending_key, "1", ex=3600)
# Verify execute was called
mock_pipe.execute.assert_called_once()
@@ -62,33 +67,48 @@ class TestRedisChannel:
def test_fetch_commands_empty(self):
"""Test fetching commands when Redis list is empty."""
mock_redis = MagicMock()
- mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context]
- # Simulate empty list
- mock_pipe.execute.return_value = [[], 1] # Empty list, delete successful
+ # No pending marker
+ pending_pipe.execute.return_value = [None, 0]
+ mock_redis.llen.return_value = 0
channel = RedisChannel(mock_redis, "test:key")
commands = channel.fetch_commands()
assert commands == []
- mock_pipe.lrange.assert_called_once_with("test:key", 0, -1)
- mock_pipe.delete.assert_called_once_with("test:key")
+ mock_redis.pipeline.assert_called_once()
+ fetch_pipe.lrange.assert_not_called()
+ fetch_pipe.delete.assert_not_called()
def test_fetch_commands_with_abort_command(self):
"""Test fetching abort commands from Redis."""
mock_redis = MagicMock()
- mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
# Create abort command data
abort_command = AbortCommand()
command_json = json.dumps(abort_command.model_dump())
# Simulate Redis returning one command
- mock_pipe.execute.return_value = [[command_json.encode()], 1]
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [[command_json.encode()], 1]
channel = RedisChannel(mock_redis, "test:key")
commands = channel.fetch_commands()
@@ -100,9 +120,15 @@ class TestRedisChannel:
def test_fetch_commands_multiple(self):
"""Test fetching multiple commands from Redis."""
mock_redis = MagicMock()
- mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
# Create multiple commands
command1 = GraphEngineCommand(command_type=CommandType.ABORT)
@@ -112,7 +138,8 @@ class TestRedisChannel:
command2_json = json.dumps(command2.model_dump())
# Simulate Redis returning multiple commands
- mock_pipe.execute.return_value = [[command1_json.encode(), command2_json.encode()], 1]
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [[command1_json.encode(), command2_json.encode()], 1]
channel = RedisChannel(mock_redis, "test:key")
commands = channel.fetch_commands()
@@ -124,9 +151,15 @@ class TestRedisChannel:
def test_fetch_commands_skips_invalid_json(self):
"""Test that invalid JSON commands are skipped."""
mock_redis = MagicMock()
- mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
# Mix valid and invalid JSON
valid_command = AbortCommand()
@@ -134,7 +167,8 @@ class TestRedisChannel:
invalid_json = b"invalid json {"
# Simulate Redis returning mixed valid/invalid commands
- mock_pipe.execute.return_value = [[invalid_json, valid_json.encode()], 1]
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [[invalid_json, valid_json.encode()], 1]
channel = RedisChannel(mock_redis, "test:key")
commands = channel.fetch_commands()
@@ -187,13 +221,20 @@ class TestRedisChannel:
def test_atomic_fetch_and_clear(self):
"""Test that fetch_commands atomically fetches and clears the list."""
mock_redis = MagicMock()
- mock_pipe = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = MagicMock(return_value=mock_pipe)
- mock_redis.pipeline.return_value.__exit__ = MagicMock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
command = AbortCommand()
command_json = json.dumps(command.model_dump())
- mock_pipe.execute.return_value = [[command_json.encode()], 1]
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [[command_json.encode()], 1]
channel = RedisChannel(mock_redis, "test:key")
@@ -202,7 +243,29 @@ class TestRedisChannel:
assert len(commands) == 1
# Verify both lrange and delete were called in the pipeline
- assert mock_pipe.lrange.call_count == 1
- assert mock_pipe.delete.call_count == 1
- mock_pipe.lrange.assert_called_with("test:key", 0, -1)
- mock_pipe.delete.assert_called_with("test:key")
+ assert fetch_pipe.lrange.call_count == 1
+ assert fetch_pipe.delete.call_count == 1
+ fetch_pipe.lrange.assert_called_with("test:key", 0, -1)
+ fetch_pipe.delete.assert_called_with("test:key")
+
+ def test_fetch_commands_without_pending_marker_returns_empty(self):
+ """Ensure we avoid unnecessary list reads when pending flag is missing."""
+ mock_redis = MagicMock()
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
+
+ # Pending flag absent
+ pending_pipe.execute.return_value = [None, 0]
+ channel = RedisChannel(mock_redis, "test:key")
+ commands = channel.fetch_commands()
+
+ assert commands == []
+ mock_redis.llen.assert_not_called()
+ assert mock_redis.pipeline.call_count == 1
diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_dispatcher.py b/api/tests/unit_tests/core/workflow/graph_engine/test_dispatcher.py
new file mode 100644
index 0000000000..830fc0884d
--- /dev/null
+++ b/api/tests/unit_tests/core/workflow/graph_engine/test_dispatcher.py
@@ -0,0 +1,104 @@
+"""Tests for dispatcher command checking behavior."""
+
+from __future__ import annotations
+
+import queue
+from datetime import datetime
+
+from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
+from core.workflow.graph_engine.event_management.event_manager import EventManager
+from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
+from core.workflow.graph_events import NodeRunStartedEvent, NodeRunSucceededEvent
+from core.workflow.node_events import NodeRunResult
+
+
+class _StubExecutionCoordinator:
+ """Stub execution coordinator that tracks command checks."""
+
+ def __init__(self) -> None:
+ self.command_checks = 0
+ self.scaling_checks = 0
+ self._execution_complete = False
+ self.mark_complete_called = False
+ self.failed = False
+
+ def check_commands(self) -> None:
+ self.command_checks += 1
+
+ def check_scaling(self) -> None:
+ self.scaling_checks += 1
+
+ def is_execution_complete(self) -> bool:
+ return self._execution_complete
+
+ def mark_complete(self) -> None:
+ self.mark_complete_called = True
+
+ def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
+ self.failed = True
+
+ def set_execution_complete(self) -> None:
+ self._execution_complete = True
+
+
+class _StubEventHandler:
+ """Minimal event handler that marks execution complete after handling an event."""
+
+ def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
+ self._coordinator = coordinator
+ self.events = []
+
+ def dispatch(self, event) -> None:
+ self.events.append(event)
+ self._coordinator.set_execution_complete()
+
+
+def _run_dispatcher_for_event(event) -> int:
+ """Run the dispatcher loop for a single event and return command check count."""
+ event_queue: queue.Queue = queue.Queue()
+ event_queue.put(event)
+
+ coordinator = _StubExecutionCoordinator()
+ event_handler = _StubEventHandler(coordinator)
+ event_manager = EventManager()
+
+ dispatcher = Dispatcher(
+ event_queue=event_queue,
+ event_handler=event_handler,
+ event_collector=event_manager,
+ execution_coordinator=coordinator,
+ )
+
+ dispatcher._dispatcher_loop()
+
+ return coordinator.command_checks
+
+
+def _make_started_event() -> NodeRunStartedEvent:
+ return NodeRunStartedEvent(
+ id="start-event",
+ node_id="node-1",
+ node_type=NodeType.CODE,
+ node_title="Test Node",
+ start_at=datetime.utcnow(),
+ )
+
+
+def _make_succeeded_event() -> NodeRunSucceededEvent:
+ return NodeRunSucceededEvent(
+ id="success-event",
+ node_id="node-1",
+ node_type=NodeType.CODE,
+ node_title="Test Node",
+ start_at=datetime.utcnow(),
+ node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
+ )
+
+
+def test_dispatcher_checks_commands_after_node_completion() -> None:
+ """Dispatcher should only check commands after node completion events."""
+ started_checks = _run_dispatcher_for_event(_make_started_event())
+ succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
+
+ assert started_checks == 0
+ assert succeeded_checks == 1
diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_redis_stop_integration.py b/api/tests/unit_tests/core/workflow/graph_engine/test_redis_stop_integration.py
index bd41fdeee5..e191246bed 100644
--- a/api/tests/unit_tests/core/workflow/graph_engine/test_redis_stop_integration.py
+++ b/api/tests/unit_tests/core/workflow/graph_engine/test_redis_stop_integration.py
@@ -132,15 +132,22 @@ class TestRedisStopIntegration:
"""Test RedisChannel correctly fetches and deserializes commands."""
# Setup
mock_redis = MagicMock()
- mock_pipeline = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = Mock(return_value=mock_pipeline)
- mock_redis.pipeline.return_value.__exit__ = Mock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
# Mock command data
abort_command_json = json.dumps({"command_type": CommandType.ABORT, "reason": "Test abort", "payload": None})
# Mock pipeline execute to return commands
- mock_pipeline.execute.return_value = [
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [
[abort_command_json.encode()], # lrange result
True, # delete result
]
@@ -158,19 +165,29 @@ class TestRedisStopIntegration:
assert commands[0].reason == "Test abort"
# Verify Redis operations
- mock_pipeline.lrange.assert_called_once_with(channel_key, 0, -1)
- mock_pipeline.delete.assert_called_once_with(channel_key)
+ pending_pipe.get.assert_called_once_with(f"{channel_key}:pending")
+ pending_pipe.delete.assert_called_once_with(f"{channel_key}:pending")
+ fetch_pipe.lrange.assert_called_once_with(channel_key, 0, -1)
+ fetch_pipe.delete.assert_called_once_with(channel_key)
+ assert mock_redis.pipeline.call_count == 2
def test_redis_channel_fetch_commands_handles_invalid_json(self):
"""Test RedisChannel gracefully handles invalid JSON in commands."""
# Setup
mock_redis = MagicMock()
- mock_pipeline = MagicMock()
- mock_redis.pipeline.return_value.__enter__ = Mock(return_value=mock_pipeline)
- mock_redis.pipeline.return_value.__exit__ = Mock(return_value=None)
+ pending_pipe = MagicMock()
+ fetch_pipe = MagicMock()
+ pending_context = MagicMock()
+ fetch_context = MagicMock()
+ pending_context.__enter__.return_value = pending_pipe
+ pending_context.__exit__.return_value = None
+ fetch_context.__enter__.return_value = fetch_pipe
+ fetch_context.__exit__.return_value = None
+ mock_redis.pipeline.side_effect = [pending_context, fetch_context]
# Mock invalid command data
- mock_pipeline.execute.return_value = [
+ pending_pipe.execute.return_value = [b"1", 1]
+ fetch_pipe.execute.return_value = [
[b"invalid json", b'{"command_type": "invalid_type"}'], # lrange result
True, # delete result
]
diff --git a/web/app/components/app-sidebar/dataset-info/menu.tsx b/web/app/components/app-sidebar/dataset-info/menu.tsx
index fd560ce643..6f91c9c513 100644
--- a/web/app/components/app-sidebar/dataset-info/menu.tsx
+++ b/web/app/components/app-sidebar/dataset-info/menu.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import MenuItem from './menu-item'
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import Divider from '../../base/divider'
+import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
type MenuProps = {
showDelete: boolean
@@ -18,6 +19,7 @@ const Menu = ({
detectIsUsedByApp,
}: MenuProps) => {
const { t } = useTranslation()
+ const runtimeMode = useDatasetDetailContextWithSelector(state => state.dataset?.runtime_mode)
return (
@@ -27,11 +29,13 @@ const Menu = ({
name={t('common.operation.edit')}
handleClick={openRenameModal}
/>
-
+ {runtimeMode === 'rag_pipeline' && (
+
+ )}
{showDelete && (
<>
diff --git a/web/app/components/app/configuration/base/icons/citation.tsx b/web/app/components/app/configuration/base/icons/citation.tsx
deleted file mode 100644
index 3aa6b0f0e1..0000000000
--- a/web/app/components/app/configuration/base/icons/citation.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { SVGProps } from 'react'
-
-const CitationIcon = (props: SVGProps) => (
-
-)
-
-export default CitationIcon
diff --git a/web/app/components/app/configuration/base/icons/more-like-this-icon.tsx b/web/app/components/app/configuration/base/icons/more-like-this-icon.tsx
deleted file mode 100644
index 74c808eb39..0000000000
--- a/web/app/components/app/configuration/base/icons/more-like-this-icon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-
-const MoreLikeThisIcon: FC = () => {
- return (
-
-
- )
-}
-export default React.memo(MoreLikeThisIcon)
diff --git a/web/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon.tsx b/web/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon.tsx
deleted file mode 100644
index cabc2e4d73..0000000000
--- a/web/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-
-const SuggestedQuestionsAfterAnswerIcon: FC = () => {
- return (
-
- )
-}
-export default React.memo(SuggestedQuestionsAfterAnswerIcon)
diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx
index 2ac68227e3..b9227c6846 100644
--- a/web/app/components/app/configuration/config-var/index.tsx
+++ b/web/app/components/app/configuration/config-var/index.tsx
@@ -1,10 +1,11 @@
'use client'
import type { FC } from 'react'
-import React, { useState } from 'react'
+import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import produce from 'immer'
+import { ReactSortable } from 'react-sortablejs'
import Panel from '../base/feature-panel'
import EditModal from './config-modal'
import VarItem from './var-item'
@@ -22,6 +23,7 @@ import { useModalContext } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
+import cn from '@/utils/classnames'
export const ADD_EXTERNAL_DATA_TOOL = 'ADD_EXTERNAL_DATA_TOOL'
@@ -218,6 +220,16 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar
showEditModal()
}
+
+ const promptVariablesWithIds = useMemo(() => promptVariables.map((item) => {
+ return {
+ id: item.key,
+ variable: { ...item },
+ }
+ }), [promptVariables])
+
+ const canDrag = !readonly && promptVariables.length > 1
+
return (
= ({ promptVariables, readonly, onPromptVar
)}
{hasVar && (
- {promptVariables.map(({ key, name, type, required, config, icon, icon_background }, index) => (
- handleConfig({ type, key, index, name, config, icon, icon_background })}
- onRemove={() => handleRemoveVar(index)}
- />
- ))}
+ { onPromptVariablesChange?.(list.map(item => item.variable)) }}
+ handle='.handle'
+ ghostClass='opacity-50'
+ animation={150}
+ >
+ {promptVariablesWithIds.map((item, index) => {
+ const { key, name, type, required, config, icon, icon_background } = item.variable
+ return (
+ handleConfig({ type, key, index, name, config, icon, icon_background })}
+ onRemove={() => handleRemoveVar(index)}
+ canDrag={canDrag}
+ />
+ )
+ })}
+
)}
diff --git a/web/app/components/app/configuration/config-var/var-item.tsx b/web/app/components/app/configuration/config-var/var-item.tsx
index 78ed4b1031..88cd5d7843 100644
--- a/web/app/components/app/configuration/config-var/var-item.tsx
+++ b/web/app/components/app/configuration/config-var/var-item.tsx
@@ -3,6 +3,7 @@ import type { FC } from 'react'
import React, { useState } from 'react'
import {
RiDeleteBinLine,
+ RiDraggable,
RiEditLine,
} from '@remixicon/react'
import type { IInputTypeIconProps } from './input-type-icon'
@@ -12,6 +13,7 @@ import Badge from '@/app/components/base/badge'
import cn from '@/utils/classnames'
type ItemProps = {
+ className?: string
readonly?: boolean
name: string
label: string
@@ -19,9 +21,11 @@ type ItemProps = {
type: string
onEdit: () => void
onRemove: () => void
+ canDrag?: boolean
}
const VarItem: FC = ({
+ className,
readonly,
name,
label,
@@ -29,12 +33,16 @@ const VarItem: FC = ({
type,
onEdit,
onRemove,
+ canDrag,
}) => {
const [isDeleting, setIsDeleting] = useState(false)
return (
-
-
+
+
+ {canDrag && (
+
+ )}
{name}
diff --git a/web/app/components/app/configuration/config/feature/use-feature.tsx b/web/app/components/app/configuration/config/feature/use-feature.tsx
deleted file mode 100644
index acc08dd4a4..0000000000
--- a/web/app/components/app/configuration/config/feature/use-feature.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { useEffect } from 'react'
-
-function useFeature({
- introduction,
- setIntroduction,
- moreLikeThis,
- setMoreLikeThis,
- suggestedQuestionsAfterAnswer,
- setSuggestedQuestionsAfterAnswer,
- speechToText,
- setSpeechToText,
- textToSpeech,
- setTextToSpeech,
- citation,
- setCitation,
- annotation,
- setAnnotation,
- moderation,
- setModeration,
-}: {
- introduction: string
- setIntroduction: (introduction: string) => void
- moreLikeThis: boolean
- setMoreLikeThis: (moreLikeThis: boolean) => void
- suggestedQuestionsAfterAnswer: boolean
- setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: boolean) => void
- speechToText: boolean
- setSpeechToText: (speechToText: boolean) => void
- textToSpeech: boolean
- setTextToSpeech: (textToSpeech: boolean) => void
- citation: boolean
- setCitation: (citation: boolean) => void
- annotation: boolean
- setAnnotation: (annotation: boolean) => void
- moderation: boolean
- setModeration: (moderation: boolean) => void
-}) {
- const [tempShowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
- useEffect(() => {
- // wait to api data back
- if (introduction)
- setTempShowOpeningStatement(true)
- }, [introduction])
-
- // const [tempMoreLikeThis, setTempMoreLikeThis] = React.useState(moreLikeThis)
- // useEffect(() => {
- // setTempMoreLikeThis(moreLikeThis)
- // }, [moreLikeThis])
-
- const featureConfig = {
- openingStatement: tempShowOpeningStatement,
- moreLikeThis,
- suggestedQuestionsAfterAnswer,
- speechToText,
- textToSpeech,
- citation,
- annotation,
- moderation,
- }
- const handleFeatureChange = (key: string, value: boolean) => {
- switch (key) {
- case 'openingStatement':
- if (!value)
- setIntroduction('')
-
- setTempShowOpeningStatement(value)
- break
- case 'moreLikeThis':
- setMoreLikeThis(value)
- break
- case 'suggestedQuestionsAfterAnswer':
- setSuggestedQuestionsAfterAnswer(value)
- break
- case 'speechToText':
- setSpeechToText(value)
- break
- case 'textToSpeech':
- setTextToSpeech(value)
- break
- case 'citation':
- setCitation(value)
- break
- case 'annotation':
- setAnnotation(value)
- break
- case 'moderation':
- setModeration(value)
- }
- }
- return {
- featureConfig,
- handleFeatureChange,
- }
-}
-
-export default useFeature
diff --git a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx
deleted file mode 100644
index f207cddd16..0000000000
--- a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { useDocLink } from '@/context/i18n'
-type Props = {
- onReturnToSimpleMode: () => void
-}
-
-const AdvancedModeWarning: FC
= ({
- onReturnToSimpleMode,
-}) => {
- const { t } = useTranslation()
- const docLink = useDocLink()
- const [show, setShow] = React.useState(true)
- if (!show)
- return null
- return (
-
-
{t('appDebug.promptMode.advancedWarning.title')}
-
-
-
-
-
-
{t('appDebug.promptMode.switchBack')}
-
-
setShow(false)}
- >{t('appDebug.promptMode.advancedWarning.ok')}
-
-
-
-
- )
-}
-export default React.memo(AdvancedModeWarning)
diff --git a/web/app/components/base/auto-height-textarea/common.tsx b/web/app/components/base/auto-height-textarea/common.tsx
deleted file mode 100644
index eb0275cfcd..0000000000
--- a/web/app/components/base/auto-height-textarea/common.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useEffect, useRef } from 'react'
-import cn from '@/utils/classnames'
-
-type AutoHeightTextareaProps
- = & React.DetailedHTMLProps, HTMLTextAreaElement>
- & { outerClassName?: string }
-
-const AutoHeightTextarea = (
- {
- ref: outRef,
- outerClassName,
- value,
- className,
- placeholder,
- autoFocus,
- disabled,
- ...rest
- }: AutoHeightTextareaProps & {
- ref: React.RefObject;
- },
-) => {
- const innerRef = useRef(null)
- const ref = outRef || innerRef
-
- useEffect(() => {
- if (autoFocus && !disabled && value) {
- if (typeof ref !== 'function') {
- ref.current?.setSelectionRange(`${value}`.length, `${value}`.length)
- ref.current?.focus()
- }
- }
- }, [autoFocus, disabled, ref])
- return (
- (
-
-
- {!value ? placeholder : `${value}`.replace(/\n$/, '\n ')}
-
-
-
-
)
- )
-}
-
-AutoHeightTextarea.displayName = 'AutoHeightTextarea'
-
-export default AutoHeightTextarea
diff --git a/web/app/components/base/chat/chat/thought/panel.tsx b/web/app/components/base/chat/chat/thought/panel.tsx
deleted file mode 100644
index 541818c8d1..0000000000
--- a/web/app/components/base/chat/chat/thought/panel.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-
-type Props = {
- isRequest: boolean
- toolName: string
- content: string
-}
-
-const Panel: FC = ({
- isRequest,
- toolName,
- content,
-}) => {
- const { t } = useTranslation()
-
- return (
-
-
- {t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName}
-
-
{content}
-
- )
-}
-export default React.memo(Panel)
diff --git a/web/app/components/base/chat/chat/thought/tool.tsx b/web/app/components/base/chat/chat/thought/tool.tsx
deleted file mode 100644
index d0a3c52a3d..0000000000
--- a/web/app/components/base/chat/chat/thought/tool.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-
-import {
- RiArrowDownSLine,
- RiLoader2Line,
-} from '@remixicon/react'
-import type { ToolInfoInThought } from '../type'
-import Panel from './panel'
-import cn from '@/utils/classnames'
-import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
-import { DataSet as DataSetIcon } from '@/app/components/base/icons/src/public/thought'
-import type { Emoji } from '@/app/components/tools/types'
-import AppIcon from '@/app/components/base/app-icon'
-
-type Props = {
- payload: ToolInfoInThought
- allToolIcons?: Record
-}
-
-const getIcon = (toolName: string, allToolIcons: Record) => {
- if (toolName.startsWith('dataset_'))
- return
- const icon = allToolIcons[toolName]
- if (!icon)
- return null
- return (
- typeof icon === 'string'
- ? (
-
- )
- : (
-
- ))
-}
-
-const Tool: FC = ({
- payload,
- allToolIcons = {},
-}) => {
- const { t } = useTranslation()
- const { name, label, input, isFinished, output } = payload
- const toolName = name.startsWith('dataset_') ? t('dataset.knowledge') : name
- const toolLabel = name.startsWith('dataset_') ? t('dataset.knowledge') : label
- const [isShowDetail, setIsShowDetail] = useState(false)
- const icon = getIcon(name, allToolIcons) as any
- return (
-
-
-
setIsShowDetail(!isShowDetail)}
- >
- {!isFinished && (
-
- )}
- {isFinished && !isShowDetail && (
-
- )}
- {isFinished && isShowDetail && (
- icon
- )}
-
- {t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
-
-
- {toolLabel}
-
-
-
- {isShowDetail && (
-
-
- {output && (
-
- )}
-
- )}
-
-
- )
-}
-export default React.memo(Tool)
diff --git a/web/app/components/base/copy-btn/index.tsx b/web/app/components/base/copy-btn/index.tsx
deleted file mode 100644
index 88c8ba60e2..0000000000
--- a/web/app/components/base/copy-btn/index.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client'
-import { useState } from 'react'
-import { t } from 'i18next'
-import { debounce } from 'lodash-es'
-import copy from 'copy-to-clipboard'
-import s from './style.module.css'
-import Tooltip from '@/app/components/base/tooltip'
-
-type ICopyBtnProps = {
- value: string
- className?: string
- isPlain?: boolean
-}
-
-const CopyBtn = ({
- value,
- className,
- isPlain,
-}: ICopyBtnProps) => {
- const [isCopied, setIsCopied] = useState(false)
-
- const onClickCopy = debounce(() => {
- copy(value)
- setIsCopied(true)
- }, 100)
-
- const onMouseLeave = debounce(() => {
- setIsCopied(false)
- }, 100)
-
- return (
-
- )
-}
-
-export default CopyBtn
diff --git a/web/app/components/base/copy-btn/style.module.css b/web/app/components/base/copy-btn/style.module.css
deleted file mode 100644
index 83625d6189..0000000000
--- a/web/app/components/base/copy-btn/style.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.copyIcon {
- background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
- background-position: center;
- background-repeat: no-repeat;
-}
-
-.copyIcon:hover {
- background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
- background-position: center;
- background-repeat: no-repeat;
-}
-
-.copyIcon.copied {
- background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
-}
diff --git a/web/app/components/base/custom-icon/index.tsx b/web/app/components/base/custom-icon/index.tsx
deleted file mode 100644
index c3afee952b..0000000000
--- a/web/app/components/base/custom-icon/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { FC } from 'react'
-import React from 'react'
-
-type IconProps = {
- icon: any
- className?: string
- [key: string]: any
-}
-
-const Icon: FC = ({ icon, className, ...other }) => {
- return (
-
- )
-}
-
-export default Icon
diff --git a/web/app/components/base/divider/with-label.tsx b/web/app/components/base/divider/with-label.tsx
deleted file mode 100644
index 0cd9796a86..0000000000
--- a/web/app/components/base/divider/with-label.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { FC } from 'react'
-import type { DividerProps } from '.'
-import Divider from '.'
-import classNames from '@/utils/classnames'
-
-export type DividerWithLabelProps = DividerProps & {
- label: string
-}
-
-export const DividerWithLabel: FC = (props) => {
- const { label, className, ...rest } = props
- return
-}
-
-export default DividerWithLabel
diff --git a/web/app/components/base/float-popover-container/index.tsx b/web/app/components/base/float-popover-container/index.tsx
deleted file mode 100644
index be5db82420..0000000000
--- a/web/app/components/base/float-popover-container/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client'
-import {
- PortalToFollowElem,
- PortalToFollowElemContent,
- PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
-import type { PortalToFollowElemOptions } from '@/app/components/base/portal-to-follow-elem'
-
-type IFloatRightContainerProps = {
- isMobile: boolean
- open: boolean
- toggle: () => void
- triggerElement?: React.ReactNode
- children?: React.ReactNode
-} & PortalToFollowElemOptions
-
-const FloatRightContainer = ({ open, toggle, triggerElement, isMobile, children, ...portalProps }: IFloatRightContainerProps) => {
- return (
- <>
- {isMobile && (
-
-
- {triggerElement}
-
-
- {children}
-
-
- )}
- {!isMobile && open && (
- <>{children}>
- )}
- >
- )
-}
-
-export default FloatRightContainer
diff --git a/web/app/components/base/install-button/index.tsx b/web/app/components/base/install-button/index.tsx
deleted file mode 100644
index 0d9e953d5e..0000000000
--- a/web/app/components/base/install-button/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Button from '../button'
-import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
-
-type InstallButtonProps = {
- loading: boolean
- onInstall: (e: React.MouseEvent) => void
- t: any
-}
-
-const InstallButton = ({ loading, onInstall, t }: InstallButtonProps) => {
- return (
-
- )
-}
-
-export default InstallButton
diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
index fd1cd01abf..d6f56b8c43 100644
--- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
+++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
@@ -118,8 +118,20 @@ const FormItem: FC = ({
{!isArrayLikeType && !isBooleanType && (
-
{typeof payload.label === 'object' ? nodeKey : payload.label}
- {!payload.required &&
{t('workflow.panel.optional')}}
+
+ {typeof payload.label === 'object' ? nodeKey : payload.label}
+
+ {payload.hide === true ? (
+
+ {t('workflow.panel.optional_and_hidden')}
+
+ ) : (
+ !payload.required && (
+
+ {t('workflow.panel.optional')}
+
+ )
+ )}
)}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx
index 447dfe1f40..5e84a518cc 100644
--- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx
+++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx
@@ -61,7 +61,6 @@ const VarList: FC
= ({
return
}
if (list.some(item => item.variable?.trim() === newKey.trim())) {
- console.log('new key', newKey.trim())
setToastHandle(Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx
index bbfeed461a..36524b482f 100644
--- a/web/app/components/workflow/nodes/start/components/var-list.tsx
+++ b/web/app/components/workflow/nodes/start/components/var-list.tsx
@@ -5,7 +5,6 @@ import produce from 'immer'
import { useTranslation } from 'react-i18next'
import VarItem from './var-item'
import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types'
-import { v4 as uuid4 } from 'uuid'
import { ReactSortable } from 'react-sortablejs'
import { RiDraggable } from '@remixicon/react'
import cn from '@/utils/classnames'
@@ -71,9 +70,8 @@ const VarList: FC = ({
}, [list, onChange])
const listWithIds = useMemo(() => list.map((item) => {
- const id = uuid4()
return {
- id,
+ id: item.variable,
variable: { ...item },
}
}), [list])
@@ -88,6 +86,8 @@ const VarList: FC = ({
)
}
+ const canDrag = !readonly && varCount > 1
+
return (
= ({
ghostClass='opacity-50'
animation={150}
>
- {list.map((item, index) => {
- const canDrag = (() => {
- if (readonly)
- return false
- return varCount > 1
- })()
- return (
-
- item.variable)}
- canDrag={canDrag}
- />
- {canDrag && }
-
- )
- })}
+ {listWithIds.map((itemWithId, index) => (
+
+ item.variable)}
+ canDrag={canDrag}
+ />
+ {canDrag && }
+
+ ))}
)
}
diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx
index baf4c21dcd..9c64849dc5 100644
--- a/web/app/components/workflow/panel/debug-and-preview/index.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx
@@ -39,7 +39,7 @@ const DebugAndPreview = () => {
const selectedNode = nodes.find(node => node.data.selected)
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || []
- const visibleVariables = variables.filter(v => v.hide !== true)
+ const visibleVariables = variables
const [showConversationVariableModal, setShowConversationVariableModal] = useState(false)
diff --git a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx
index 0670d9a52b..b7ed2a54fd 100644
--- a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx
@@ -14,10 +14,11 @@ import cn from '@/utils/classnames'
const UserInput = () => {
const workflowStore = useWorkflowStore()
const inputs = useStore(s => s.inputs)
+ const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const nodes = useNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || []
- const visibleVariables = variables.filter(v => v.hide !== true)
+ const visibleVariables = showDebugAndPreviewPanel ? variables : variables.filter(v => v.hide !== true)
const handleValueChange = (variable: string, v: string) => {
const {
diff --git a/web/i18n/de-DE/dataset.ts b/web/i18n/de-DE/dataset.ts
index fbb52878f8..0b9d08a984 100644
--- a/web/i18n/de-DE/dataset.ts
+++ b/web/i18n/de-DE/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Aktualisierte',
externalKnowledgeBase: 'Externe Wissensdatenbank',
createFromPipeline: 'Aus Wissenspipeline erstellen',
+ serviceApi: {
+ card: {
+ title: 'Backend-Dienst-API',
+ apiReference: 'API Referenz',
+ apiKey: 'API-Schlüssel',
+ endpoint: 'Service-API-Endpunkt',
+ },
+ title: 'Service-API',
+ enabled: 'Im Dienst',
+ disabled: 'Behindert',
+ },
}
export default translation
diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts
index 63bce527ca..d943c6eb06 100644
--- a/web/i18n/de-DE/workflow.ts
+++ b/web/i18n/de-DE/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximiere die Leinwand',
minimize: 'Vollbildmodus beenden',
scrollToSelectedNode: 'Zum ausgewählten Knoten scrollen',
+ optional_and_hidden: '(optional & hidden)',
},
nodes: {
common: {
diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts
index f0ad06121c..b89a1fbd34 100644
--- a/web/i18n/en-US/dataset.ts
+++ b/web/i18n/en-US/dataset.ts
@@ -90,8 +90,8 @@ const translation = {
intro2: 'as a context',
intro3: ',',
intro4: 'or it ',
- intro5: 'can be created',
- intro6: ' as a standalone ChatGPT index plug-in to publish',
+ intro5: 'can be published',
+ intro6: ' as an independent service.',
unavailable: 'Unavailable',
unavailableTip: 'Embedding model is not available, the default embedding model needs to be configured',
datasets: 'KNOWLEDGE',
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index 67b8ae5f2c..c72afb228f 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -371,6 +371,7 @@ const translation = {
maximize: 'Maximize Canvas',
minimize: 'Exit Full Screen',
scrollToSelectedNode: 'Scroll to selected node',
+ optional_and_hidden: '(optional & hidden)',
},
nodes: {
common: {
diff --git a/web/i18n/es-ES/dataset.ts b/web/i18n/es-ES/dataset.ts
index 785e2c962b..4fbdae1239 100644
--- a/web/i18n/es-ES/dataset.ts
+++ b/web/i18n/es-ES/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
externalKnowledgeBase: 'Base de conocimientos externa',
createFromPipeline: 'Crear desde Knowledge Pipeline',
updated: 'Actualizado',
+ serviceApi: {
+ card: {
+ apiReference: 'Referencia de la API',
+ apiKey: 'Clave API',
+ endpoint: 'Punto de enlace de la API de servicio',
+ title: 'API del servicio de backend',
+ },
+ enabled: 'En servicio',
+ title: 'API de servicios',
+ disabled: 'Discapacitado',
+ },
}
export default translation
diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts
index a423e4479e..2e3363d36a 100644
--- a/web/i18n/es-ES/workflow.ts
+++ b/web/i18n/es-ES/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximizar Canvas',
minimize: 'Salir de pantalla completa',
scrollToSelectedNode: 'Desplácese hasta el nodo seleccionado',
+ optional_and_hidden: '(opcional y oculto)',
},
nodes: {
common: {
diff --git a/web/i18n/fa-IR/dataset.ts b/web/i18n/fa-IR/dataset.ts
index b4a6f1f97d..f0c1a69044 100644
--- a/web/i18n/fa-IR/dataset.ts
+++ b/web/i18n/fa-IR/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'بروز رسانی',
createFromPipeline: 'ایجاد از پایپ لاین دانش',
externalKnowledgeBase: 'پایگاه دانش خارجی',
+ serviceApi: {
+ card: {
+ apiKey: 'کلید API',
+ title: 'رابط برنامهنویسی سرویس پشتیبان',
+ apiReference: 'مرجع API',
+ endpoint: 'نقطه انتهایی رابط برنامهنویسی سرویس',
+ },
+ disabled: 'معلول',
+ enabled: 'در حال خدمت',
+ title: 'رابط برنامهنویسی سرویس',
+ },
}
export default translation
diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts
index db9544f8c4..63cbdbd6f2 100644
--- a/web/i18n/fa-IR/workflow.ts
+++ b/web/i18n/fa-IR/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'خروج از حالت تمام صفحه',
maximize: 'بیشینهسازی بوم',
scrollToSelectedNode: 'به گره انتخاب شده بروید',
+ optional_and_hidden: '(اختیاری و پنهان)',
},
nodes: {
common: {
diff --git a/web/i18n/fr-FR/dataset.ts b/web/i18n/fr-FR/dataset.ts
index d267356ce7..2a18ae9f6b 100644
--- a/web/i18n/fr-FR/dataset.ts
+++ b/web/i18n/fr-FR/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Actualisé',
createFromPipeline: 'Créer à partir du pipeline de connaissances',
externalKnowledgeBase: 'Base de connaissances externe',
+ serviceApi: {
+ card: {
+ apiKey: 'Clé API',
+ apiReference: 'Référence API',
+ title: 'API du service backend',
+ endpoint: 'Point de terminaison de l\'API',
+ },
+ enabled: 'En service',
+ title: 'API de service',
+ disabled: 'désactivé',
+ },
}
export default translation
diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts
index eab8c5d359..a3b9d9b8ab 100644
--- a/web/i18n/fr-FR/workflow.ts
+++ b/web/i18n/fr-FR/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximiser le Canvas',
minimize: 'Sortir du mode plein écran',
scrollToSelectedNode: 'Faites défiler jusqu’au nœud sélectionné',
+ optional_and_hidden: '(optionnel et caché)',
},
nodes: {
common: {
diff --git a/web/i18n/hi-IN/dataset.ts b/web/i18n/hi-IN/dataset.ts
index 360b57d21d..fa1948c497 100644
--- a/web/i18n/hi-IN/dataset.ts
+++ b/web/i18n/hi-IN/dataset.ts
@@ -233,6 +233,17 @@ const translation = {
updated: 'अपडेट किया गया',
externalKnowledgeBase: 'बाहरी ज्ञान आधार',
createFromPipeline: 'ज्ञान पाइपलाइन से बनाएं',
+ serviceApi: {
+ card: {
+ apiReference: 'एपीआई संदर्भ',
+ apiKey: 'एपीआई कुंजी',
+ title: 'बैकएंड सेवा एपीआई',
+ endpoint: 'सेवा एपीआई एंडपॉइंट',
+ },
+ enabled: 'सेवा में',
+ disabled: 'अक्षम',
+ title: 'सेवा एपीआई',
+ },
}
export default translation
diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts
index df75a4e367..ba12e9e72a 100644
--- a/web/i18n/hi-IN/workflow.ts
+++ b/web/i18n/hi-IN/workflow.ts
@@ -338,6 +338,7 @@ const translation = {
minimize: 'पूर्ण स्क्रीन से बाहर निकलें',
maximize: 'कैनवास का अधिकतम लाभ उठाएँ',
scrollToSelectedNode: 'चुने गए नोड पर स्क्रॉल करें',
+ optional_and_hidden: '(वैकल्पिक और छिपा हुआ)',
},
nodes: {
common: {
diff --git a/web/i18n/id-ID/dataset.ts b/web/i18n/id-ID/dataset.ts
index ac421c4d1a..4c41fb0942 100644
--- a/web/i18n/id-ID/dataset.ts
+++ b/web/i18n/id-ID/dataset.ts
@@ -219,6 +219,17 @@ const translation = {
updated: 'Diperbarui',
createFromPipeline: 'Membuat dari Knowledge Pipeline',
externalKnowledgeBase: 'Basis Pengetahuan Eksternal',
+ serviceApi: {
+ card: {
+ apiKey: 'Kunci API',
+ apiReference: 'Referensi API',
+ title: 'API layanan backend',
+ endpoint: 'Titik Akhir API Layanan',
+ },
+ title: 'API Layanan',
+ enabled: 'Sedang Beroperasi',
+ disabled: 'Dinonaktifkan',
+ },
}
export default translation
diff --git a/web/i18n/id-ID/workflow.ts b/web/i18n/id-ID/workflow.ts
index 969b7bb8b0..2ec7ace93f 100644
--- a/web/i18n/id-ID/workflow.ts
+++ b/web/i18n/id-ID/workflow.ts
@@ -325,6 +325,7 @@ const translation = {
changeBlock: 'Ubah Node',
runThisStep: 'Jalankan langkah ini',
maximize: 'Maksimalkan Kanvas',
+ optional_and_hidden: '(opsional & tersembunyi)',
},
nodes: {
common: {
diff --git a/web/i18n/it-IT/dataset.ts b/web/i18n/it-IT/dataset.ts
index 99a21f12e0..7489034e53 100644
--- a/web/i18n/it-IT/dataset.ts
+++ b/web/i18n/it-IT/dataset.ts
@@ -233,6 +233,17 @@ const translation = {
updated: 'Aggiornato',
externalKnowledgeBase: 'Base di conoscenza esterna',
createFromPipeline: 'Creazione da pipeline di conoscenza',
+ serviceApi: {
+ card: {
+ endpoint: 'Endpoint dell\'API di servizio',
+ apiKey: 'Chiave API',
+ title: 'API del servizio backend',
+ apiReference: 'Riferimento API',
+ },
+ disabled: 'Disabilitato',
+ title: 'API di servizio',
+ enabled: 'In servizio',
+ },
}
export default translation
diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts
index eeb8472428..107469901a 100644
--- a/web/i18n/it-IT/workflow.ts
+++ b/web/i18n/it-IT/workflow.ts
@@ -341,6 +341,7 @@ const translation = {
minimize: 'Esci dalla modalità schermo intero',
maximize: 'Massimizza Canvas',
scrollToSelectedNode: 'Scorri fino al nodo selezionato',
+ optional_and_hidden: '(opzionale e nascosto)',
},
nodes: {
common: {
diff --git a/web/i18n/ja-JP/dataset.ts b/web/i18n/ja-JP/dataset.ts
index 1e96961751..02afcd453a 100644
--- a/web/i18n/ja-JP/dataset.ts
+++ b/web/i18n/ja-JP/dataset.ts
@@ -87,8 +87,8 @@ const translation = {
intro2: 'コンテキストとして',
intro3: '、',
intro4: 'または',
- intro5: '作成することができます',
- intro6: '単体の ChatGPT インデックスプラグインとして公開するために',
+ intro5: '公開することができます',
+ intro6: '独立したサービスとして',
unavailable: '利用不可',
unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります',
datasets: 'ナレッジベース',
diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts
index f28f986b62..6fc037564e 100644
--- a/web/i18n/ja-JP/workflow.ts
+++ b/web/i18n/ja-JP/workflow.ts
@@ -367,6 +367,7 @@ const translation = {
maximize: 'キャンバスを最大化する',
minimize: '全画面を終了する',
scrollToSelectedNode: '選択したノードまでスクロール',
+ optional_and_hidden: '(オプションおよび非表示)',
},
nodes: {
common: {
diff --git a/web/i18n/ko-KR/dataset.ts b/web/i18n/ko-KR/dataset.ts
index 2bbed9a339..7f6153f968 100644
--- a/web/i18n/ko-KR/dataset.ts
+++ b/web/i18n/ko-KR/dataset.ts
@@ -225,6 +225,17 @@ const translation = {
updated: '업데이트',
externalKnowledgeBase: '외부 기술 자료',
createFromPipeline: '지식 파이프라인에서 만들기',
+ serviceApi: {
+ card: {
+ apiReference: 'API 참고',
+ endpoint: '서비스 API 엔드포인트',
+ apiKey: 'API 키',
+ title: '백엔드 서비스 API',
+ },
+ enabled: '서비스 중',
+ title: '서비스 API',
+ disabled: '장애인',
+ },
}
export default translation
diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts
index 5012e4f660..313ce3a76f 100644
--- a/web/i18n/ko-KR/workflow.ts
+++ b/web/i18n/ko-KR/workflow.ts
@@ -347,6 +347,7 @@ const translation = {
minimize: '전체 화면 종료',
maximize: '캔버스 전체 화면',
scrollToSelectedNode: '선택한 노드로 스크롤',
+ optional_and_hidden: '(선택 사항 및 숨김)',
},
nodes: {
common: {
diff --git a/web/i18n/pl-PL/dataset.ts b/web/i18n/pl-PL/dataset.ts
index 4267736a35..5c1d3630e9 100644
--- a/web/i18n/pl-PL/dataset.ts
+++ b/web/i18n/pl-PL/dataset.ts
@@ -232,6 +232,17 @@ const translation = {
updated: 'Aktualizowano',
createFromPipeline: 'Tworzenie na podstawie potoku wiedzy',
externalKnowledgeBase: 'Zewnętrzna baza wiedzy',
+ serviceApi: {
+ card: {
+ apiKey: 'Klucz API',
+ title: 'Usługa backendowa API',
+ apiReference: 'Dokumentacja API',
+ endpoint: 'Punkt końcowy API usługi',
+ },
+ title: 'Interfejs API usługi',
+ disabled: 'Niepełnosprawny',
+ enabled: 'W serwisie',
+ },
}
export default translation
diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts
index 02ef9278a6..15dd19f867 100644
--- a/web/i18n/pl-PL/workflow.ts
+++ b/web/i18n/pl-PL/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'Wyjdź z trybu pełnoekranowego',
maximize: 'Maksymalizuj płótno',
scrollToSelectedNode: 'Przewiń do wybranego węzła',
+ optional_and_hidden: '(opcjonalne i ukryte)',
},
nodes: {
common: {
diff --git a/web/i18n/pt-BR/dataset.ts b/web/i18n/pt-BR/dataset.ts
index ebbf0a1d0d..0983eddcf6 100644
--- a/web/i18n/pt-BR/dataset.ts
+++ b/web/i18n/pt-BR/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Atualizado',
externalKnowledgeBase: 'Base de conhecimento externa',
createFromPipeline: 'Criar a partir do pipeline de conhecimento',
+ serviceApi: {
+ card: {
+ apiKey: 'Chave de API',
+ apiReference: 'Referência da API',
+ title: 'API de serviço de backend',
+ endpoint: 'Endpoint da API de Serviço',
+ },
+ enabled: 'Em serviço',
+ title: 'API de Serviço',
+ disabled: 'Desativado',
+ },
}
export default translation
diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts
index c693c47965..1786844e99 100644
--- a/web/i18n/pt-BR/workflow.ts
+++ b/web/i18n/pt-BR/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximize Canvas',
minimize: 'Sair do Modo Tela Cheia',
scrollToSelectedNode: 'Role até o nó selecionado',
+ optional_and_hidden: '(opcional & oculto)',
},
nodes: {
common: {
diff --git a/web/i18n/ro-RO/dataset.ts b/web/i18n/ro-RO/dataset.ts
index dcdc081779..29efbd10fc 100644
--- a/web/i18n/ro-RO/dataset.ts
+++ b/web/i18n/ro-RO/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Actualizat',
externalKnowledgeBase: 'Baza de cunoștințe externă',
createFromPipeline: 'Crearea din Knowledge Pipeline',
+ serviceApi: {
+ card: {
+ title: 'API pentru serviciul backend',
+ apiReference: 'Referință API',
+ endpoint: 'Punct final API de servicii',
+ apiKey: 'Cheie API',
+ },
+ disabled: 'Dezactivat',
+ enabled: 'În serviciu',
+ title: 'API de servicii',
+ },
}
export default translation
diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts
index 3d7e5218cd..a572db4526 100644
--- a/web/i18n/ro-RO/workflow.ts
+++ b/web/i18n/ro-RO/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Maximize Canvas',
minimize: 'Iesi din modul pe tot ecranul',
scrollToSelectedNode: 'Derulați la nodul selectat',
+ optional_and_hidden: '(opțional și ascuns)',
},
nodes: {
common: {
diff --git a/web/i18n/ru-RU/dataset.ts b/web/i18n/ru-RU/dataset.ts
index d1701b4be5..1b8c8d4c31 100644
--- a/web/i18n/ru-RU/dataset.ts
+++ b/web/i18n/ru-RU/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Обновлено',
externalKnowledgeBase: 'Внешняя база знаний',
createFromPipeline: 'Создание из конвейера знаний',
+ serviceApi: {
+ card: {
+ apiReference: 'Справочник API',
+ title: 'API бэкенд-сервиса',
+ apiKey: 'API ключ',
+ endpoint: 'Конечная точка API сервиса',
+ },
+ enabled: 'На службе',
+ title: 'Сервисный API',
+ disabled: 'Отключено',
+ },
}
export default translation
diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts
index 71fe3853ba..5e44156796 100644
--- a/web/i18n/ru-RU/workflow.ts
+++ b/web/i18n/ru-RU/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'Выйти из полноэкранного режима',
maximize: 'Максимизировать холст',
scrollToSelectedNode: 'Прокрутите до выбранного узла',
+ optional_and_hidden: '(необязательно и скрыто)',
},
nodes: {
common: {
diff --git a/web/i18n/sl-SI/dataset.ts b/web/i18n/sl-SI/dataset.ts
index fab7a11241..cc84adf851 100644
--- a/web/i18n/sl-SI/dataset.ts
+++ b/web/i18n/sl-SI/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
createFromPipeline: 'Ustvarjanje iz cevovoda znanja',
updated: 'Posodobljene',
externalKnowledgeBase: 'Zunanja baza znanja',
+ serviceApi: {
+ card: {
+ apiKey: 'API ključ',
+ endpoint: 'Vhodna točka API storitve',
+ title: 'API storitev za zaledje',
+ apiReference: 'API Referenca',
+ },
+ title: 'Storitveni API',
+ disabled: 'Onemogočeno',
+ enabled: 'V storitvi',
+ },
}
export default translation
diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts
index b4fe903d2a..1b5a33f978 100644
--- a/web/i18n/sl-SI/workflow.ts
+++ b/web/i18n/sl-SI/workflow.ts
@@ -333,6 +333,7 @@ const translation = {
maximize: 'Maksimiziraj platno',
optional: '(neobvezno)',
scrollToSelectedNode: 'Pomaknite se do izbranega vozlišča',
+ optional_and_hidden: '(neobvezno in skrito)',
},
nodes: {
common: {
diff --git a/web/i18n/th-TH/dataset.ts b/web/i18n/th-TH/dataset.ts
index 1a49ba1626..58ddf8ba8e 100644
--- a/web/i18n/th-TH/dataset.ts
+++ b/web/i18n/th-TH/dataset.ts
@@ -225,6 +225,17 @@ const translation = {
updated: 'ปรับ ปรุง',
externalKnowledgeBase: 'ฐานความรู้ภายนอก',
createFromPipeline: 'สร้างจากไปป์ไลน์ความรู้',
+ serviceApi: {
+ card: {
+ title: 'บริการแบ็กเอนด์ API',
+ apiReference: 'เอกสารอ้างอิง API',
+ apiKey: 'กุญแจ API',
+ endpoint: 'จุดเชื่อมต่อ API บริการ',
+ },
+ enabled: 'ให้บริการ',
+ disabled: 'ถูกปิดใช้งาน',
+ title: 'บริการ API',
+ },
}
export default translation
diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts
index 4efc63fad2..03ab811044 100644
--- a/web/i18n/th-TH/workflow.ts
+++ b/web/i18n/th-TH/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'ออกจากโหมดเต็มหน้าจอ',
maximize: 'เพิ่มประสิทธิภาพผ้าใบ',
scrollToSelectedNode: 'เลื่อนไปยังโหนดที่เลือก',
+ optional_and_hidden: '(ตัวเลือก & ซ่อน)',
},
nodes: {
common: {
diff --git a/web/i18n/tr-TR/dataset.ts b/web/i18n/tr-TR/dataset.ts
index fea5ab5bb0..e290dfe711 100644
--- a/web/i18n/tr-TR/dataset.ts
+++ b/web/i18n/tr-TR/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Güncel -leştirilmiş',
createFromPipeline: 'Bilgi İşlem Hattından Oluşturun',
externalKnowledgeBase: 'Harici Bilgi Bankası',
+ serviceApi: {
+ card: {
+ apiReference: 'API Referansı',
+ title: 'Backend servis api',
+ apiKey: 'API Anahtarı',
+ endpoint: 'Hizmet API Uç Noktası',
+ },
+ disabled: 'Engelli',
+ enabled: 'Hizmette',
+ title: 'Servis API\'si',
+ },
}
export default translation
diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts
index 7a1107e48b..4d0d2d2110 100644
--- a/web/i18n/tr-TR/workflow.ts
+++ b/web/i18n/tr-TR/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'Tam Ekrandan Çık',
maximize: 'Kanvası Maksimize Et',
scrollToSelectedNode: 'Seçili düğüme kaydırma',
+ optional_and_hidden: '(isteğe bağlı ve gizli)',
},
nodes: {
common: {
diff --git a/web/i18n/uk-UA/dataset.ts b/web/i18n/uk-UA/dataset.ts
index 5732d80718..61972ac565 100644
--- a/web/i18n/uk-UA/dataset.ts
+++ b/web/i18n/uk-UA/dataset.ts
@@ -227,6 +227,17 @@ const translation = {
updated: 'Оновлено',
createFromPipeline: 'Створюйте на основі Knowledge Pipeline',
externalKnowledgeBase: 'Зовнішня база знань',
+ serviceApi: {
+ card: {
+ title: 'API бекенд-сервіс',
+ apiReference: 'Посилання на API',
+ apiKey: 'Ключ API',
+ endpoint: 'Кінцева точка API сервісу',
+ },
+ disabled: 'Вимкнено',
+ enabled: 'У службі',
+ title: 'Сервісний API',
+ },
}
export default translation
diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts
index 9140171323..d08b0ed77c 100644
--- a/web/i18n/uk-UA/workflow.ts
+++ b/web/i18n/uk-UA/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: 'Вийти з повноекранного режиму',
maximize: 'Максимізувати полотно',
scrollToSelectedNode: 'Прокрутіть до вибраного вузла',
+ optional_and_hidden: '(необов\'язково & приховано)',
},
nodes: {
common: {
diff --git a/web/i18n/vi-VN/dataset.ts b/web/i18n/vi-VN/dataset.ts
index 695e97e6ea..e5ffd5b61b 100644
--- a/web/i18n/vi-VN/dataset.ts
+++ b/web/i18n/vi-VN/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
updated: 'Cập nhật',
createFromPipeline: 'Tạo từ quy trình kiến thức',
externalKnowledgeBase: 'Cơ sở kiến thức bên ngoài',
+ serviceApi: {
+ card: {
+ title: 'API dịch vụ backend',
+ endpoint: 'Điểm cuối API dịch vụ',
+ apiKey: 'Khóa API',
+ apiReference: 'Tham chiếu API',
+ },
+ enabled: 'Đang phục vụ',
+ disabled: 'Vô hiệu hóa',
+ title: 'Giao diện lập trình dịch vụ',
+ },
}
export default translation
diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts
index 4d8c26231a..a59bdc7df0 100644
--- a/web/i18n/vi-VN/workflow.ts
+++ b/web/i18n/vi-VN/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
maximize: 'Tối đa hóa Canvas',
minimize: 'Thoát chế độ toàn màn hình',
scrollToSelectedNode: 'Cuộn đến nút đã chọn',
+ optional_and_hidden: '(tùy chọn & ẩn)',
},
nodes: {
common: {
diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts
index 641925a425..69a92b5529 100644
--- a/web/i18n/zh-Hans/dataset.ts
+++ b/web/i18n/zh-Hans/dataset.ts
@@ -90,8 +90,8 @@ const translation = {
intro2: '作为上下文',
intro3: ',',
intro4: '或可以',
- intro5: '创建',
- intro6: '为独立的 ChatGPT 插件发布使用',
+ intro5: '发布',
+ intro6: '为独立的服务',
unavailable: '不可用',
unavailableTip: '由于 embedding 模型不可用,需要配置默认 embedding 模型',
datasets: '知识库',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index df63774ebd..97e3038961 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -370,6 +370,7 @@ const translation = {
maximize: '最大化画布',
minimize: '退出最大化',
scrollToSelectedNode: '滚动至选中节点',
+ optional_and_hidden: '(选填 & 隐藏)',
},
nodes: {
common: {
diff --git a/web/i18n/zh-Hant/dataset.ts b/web/i18n/zh-Hant/dataset.ts
index fcc16963fa..f037ec7a70 100644
--- a/web/i18n/zh-Hant/dataset.ts
+++ b/web/i18n/zh-Hant/dataset.ts
@@ -226,6 +226,17 @@ const translation = {
externalKnowledgeBase: '外部知識庫',
createFromPipeline: '從知識管線建立',
updated: '更新時間',
+ serviceApi: {
+ card: {
+ title: '後端服務 API',
+ apiReference: 'API 參考',
+ endpoint: '服務 API 端點',
+ apiKey: 'API 金鑰',
+ },
+ enabled: '使用中',
+ title: '服務 API',
+ disabled: '已停用',
+ },
}
export default translation
diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts
index 9f4e7f8a87..6adfb2ec07 100644
--- a/web/i18n/zh-Hant/workflow.ts
+++ b/web/i18n/zh-Hant/workflow.ts
@@ -326,6 +326,7 @@ const translation = {
minimize: '退出全螢幕',
maximize: '最大化畫布',
scrollToSelectedNode: '捲動至選取的節點',
+ optional_and_hidden: '(可選且隱藏)',
},
nodes: {
common: {
diff --git a/web/package.json b/web/package.json
index 8fbefe6d9f..1b73a837d2 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,7 +2,7 @@
"name": "dify-web",
"version": "1.9.1",
"private": true,
- "packageManager": "pnpm@10.18.2",
+ "packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d",
"engines": {
"node": ">=v22.11.0"
},
@@ -27,6 +27,7 @@
"lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
"lint:quiet": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --quiet",
"lint:complexity": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --rule 'complexity: [error, {max: 15}]' --quiet",
+ "type-check": "tsc --noEmit",
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky",
"gen-icons": "node ./app/components/base/icons/script.mjs",
"uglify-embed": "node ./bin/uglify-embed",