diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index f54f5d6c64..e33fbb209e 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -1,7 +1,10 @@ name: Run VDB Tests on: - workflow_call: + push: + branches: [main] + paths: + - 'api/core/rag/*.py' concurrency: group: vdb-tests-${{ github.head_ref || github.run_id }} diff --git a/api/.env.example b/api/.env.example index b1ac15d25b..5713095374 100644 --- a/api/.env.example +++ b/api/.env.example @@ -159,8 +159,7 @@ SUPABASE_URL=your-server-url # CORS configuration WEB_API_CORS_ALLOW_ORIGINS=http://localhost:3000,* CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,* -# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains. -# Provide the registrable domain (e.g. example.com); leading dots are optional. +# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). Leading dots are optional. COOKIE_DOMAIN= # Vector database configuration diff --git a/api/README.md b/api/README.md index 45dad07af0..7809ea8a3d 100644 --- a/api/README.md +++ b/api/README.md @@ -26,6 +26,10 @@ cp .env.example .env ``` +> [!IMPORTANT] +> +> When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). The frontend and backend must be under the same top-level domain in order to share authentication cookies. + 1. Generate a `SECRET_KEY` in the `.env` file. bash for Linux diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 7071a1f33a..98e1a20044 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -192,7 +192,6 @@ class GraphEngine: self._dispatcher = Dispatcher( event_queue=self._event_queue, event_handler=self._event_handler_registry, - event_collector=self._event_manager, execution_coordinator=self._execution_coordinator, event_emitter=self._event_manager, ) diff --git a/api/core/workflow/graph_engine/orchestration/dispatcher.py b/api/core/workflow/graph_engine/orchestration/dispatcher.py index 4097cead9c..334a3f77bf 100644 --- a/api/core/workflow/graph_engine/orchestration/dispatcher.py +++ b/api/core/workflow/graph_engine/orchestration/dispatcher.py @@ -43,7 +43,6 @@ class Dispatcher: self, event_queue: queue.Queue[GraphNodeEventBase], event_handler: "EventHandler", - event_collector: EventManager, execution_coordinator: ExecutionCoordinator, event_emitter: EventManager | None = None, ) -> None: @@ -53,13 +52,11 @@ class Dispatcher: Args: event_queue: Queue of events from workers event_handler: Event handler registry for processing events - event_collector: Event manager for collecting unhandled events execution_coordinator: Coordinator for execution flow event_emitter: Optional event manager to signal completion """ self._event_queue = event_queue self._event_handler = event_handler - self._event_collector = event_collector self._execution_coordinator = execution_coordinator self._event_emitter = event_emitter @@ -86,37 +83,31 @@ class Dispatcher: def _dispatcher_loop(self) -> None: """Main dispatcher loop.""" try: + self._process_commands() while not self._stop_event.is_set(): - commands_checked = False - should_check_commands = False - should_break = False + if ( + self._execution_coordinator.aborted + or self._execution_coordinator.paused + or self._execution_coordinator.execution_complete + ): + break - if self._execution_coordinator.is_execution_complete(): - should_check_commands = True - should_break = True - else: - # Check for scaling - self._execution_coordinator.check_scaling() + self._execution_coordinator.check_scaling() + try: + event = self._event_queue.get(timeout=0.1) + self._event_handler.dispatch(event) + self._event_queue.task_done() + self._process_commands(event) + except queue.Empty: + time.sleep(0.1) - # Process events - try: - event = self._event_queue.get(timeout=0.1) - # Route to the event handler - self._event_handler.dispatch(event) - should_check_commands = self._should_check_commands(event) - self._event_queue.task_done() - except queue.Empty: - # Process commands even when no new events arrive so abort requests are not missed - should_check_commands = True - time.sleep(0.1) - - if should_check_commands and not commands_checked: - self._execution_coordinator.check_commands() - commands_checked = True - - if should_break: - if not commands_checked: - self._execution_coordinator.check_commands() + self._process_commands() + while True: + try: + event = self._event_queue.get(block=False) + self._event_handler.dispatch(event) + self._event_queue.task_done() + except queue.Empty: break except Exception as e: @@ -129,6 +120,6 @@ class Dispatcher: 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) + def _process_commands(self, event: GraphNodeEventBase | None = None): + if event is None or isinstance(event, self._COMMAND_TRIGGER_EVENTS): + self._execution_coordinator.process_commands() diff --git a/api/core/workflow/graph_engine/orchestration/execution_coordinator.py b/api/core/workflow/graph_engine/orchestration/execution_coordinator.py index a3162de244..e8e8f9f16c 100644 --- a/api/core/workflow/graph_engine/orchestration/execution_coordinator.py +++ b/api/core/workflow/graph_engine/orchestration/execution_coordinator.py @@ -40,7 +40,7 @@ class ExecutionCoordinator: self._command_processor = command_processor self._worker_pool = worker_pool - def check_commands(self) -> None: + def process_commands(self) -> None: """Process any pending commands.""" self._command_processor.process_commands() @@ -48,24 +48,16 @@ class ExecutionCoordinator: """Check and perform worker scaling if needed.""" self._worker_pool.check_and_scale() - def is_execution_complete(self) -> bool: - """ - Check if execution is complete. - - Returns: - True if execution is complete - """ - # Treat paused, aborted, or failed executions as terminal states - if self._graph_execution.is_paused: - return True - - if self._graph_execution.aborted or self._graph_execution.has_error: - return True - + @property + def execution_complete(self): return self._state_manager.is_execution_complete() @property - def is_paused(self) -> bool: + def aborted(self): + return self._graph_execution.aborted or self._graph_execution.has_error + + @property + def paused(self) -> bool: """Expose whether the underlying graph execution is paused.""" return self._graph_execution.is_paused diff --git a/api/tasks/batch_clean_document_task.py b/api/tasks/batch_clean_document_task.py index 447443703a..3e1bd16cc7 100644 --- a/api/tasks/batch_clean_document_task.py +++ b/api/tasks/batch_clean_document_task.py @@ -9,7 +9,7 @@ from core.rag.index_processor.index_processor_factory import IndexProcessorFacto from core.tools.utils.web_reader_tool import get_image_upload_file_ids from extensions.ext_database import db from extensions.ext_storage import storage -from models.dataset import Dataset, DocumentSegment +from models.dataset import Dataset, DatasetMetadataBinding, DocumentSegment from models.model import UploadFile logger = logging.getLogger(__name__) @@ -37,6 +37,11 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form if not dataset: raise Exception("Document has no dataset") + db.session.query(DatasetMetadataBinding).where( + DatasetMetadataBinding.dataset_id == dataset_id, + DatasetMetadataBinding.document_id.in_(document_ids), + ).delete(synchronize_session=False) + segments = db.session.scalars( select(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) ).all() @@ -71,7 +76,8 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form except Exception: logger.exception("Delete file failed when document deleted, file_id: %s", file.id) db.session.delete(file) - db.session.commit() + + db.session.commit() end_at = time.perf_counter() logger.info( diff --git a/api/tests/unit_tests/core/workflow/graph_engine/orchestration/test_dispatcher.py b/api/tests/unit_tests/core/workflow/graph_engine/orchestration/test_dispatcher.py new file mode 100644 index 0000000000..e6d4508fdf --- /dev/null +++ b/api/tests/unit_tests/core/workflow/graph_engine/orchestration/test_dispatcher.py @@ -0,0 +1,189 @@ +"""Tests for dispatcher command checking behavior.""" + +from __future__ import annotations + +import queue +from datetime import datetime +from unittest import mock + +from core.workflow.entities.pause_reason import SchedulingPause +from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus +from core.workflow.graph_engine.event_management.event_handlers import EventHandler +from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher +from core.workflow.graph_engine.orchestration.execution_coordinator import ExecutionCoordinator +from core.workflow.graph_events import ( + GraphNodeEventBase, + NodeRunPauseRequestedEvent, + NodeRunStartedEvent, + NodeRunSucceededEvent, +) +from core.workflow.node_events import NodeRunResult + + +def test_dispatcher_should_consume_remains_events_after_pause(): + event_queue = queue.Queue() + event_queue.put( + GraphNodeEventBase( + id="test", + node_id="test", + node_type=NodeType.START, + ) + ) + event_handler = mock.Mock(spec=EventHandler) + execution_coordinator = mock.Mock(spec=ExecutionCoordinator) + execution_coordinator.paused.return_value = True + dispatcher = Dispatcher( + event_queue=event_queue, + event_handler=event_handler, + execution_coordinator=execution_coordinator, + ) + dispatcher._dispatcher_loop() + assert event_queue.empty() + + +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.failed = False + self._paused = False + + def process_commands(self) -> None: + self.command_checks += 1 + + def check_scaling(self) -> None: + self.scaling_checks += 1 + + @property + def paused(self) -> bool: + return self._paused + + @property + def aborted(self) -> bool: + return False + + def mark_complete(self) -> None: + self.execution_complete = True + + def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests + self.failed = 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.mark_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) + + dispatcher = Dispatcher( + event_queue=event_queue, + event_handler=event_handler, + 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_during_idle_and_on_completion() -> None: + """Dispatcher polls commands when idle and after completion events.""" + started_checks = _run_dispatcher_for_event(_make_started_event()) + succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event()) + + assert started_checks == 2 + assert succeeded_checks == 3 + + +class _PauseStubEventHandler: + """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) + if isinstance(event, NodeRunPauseRequestedEvent): + self._coordinator.mark_complete() + + +def test_dispatcher_drain_event_queue(): + events = [ + NodeRunStartedEvent( + id="start-event", + node_id="node-1", + node_type=NodeType.CODE, + node_title="Code", + start_at=datetime.utcnow(), + ), + NodeRunPauseRequestedEvent( + id="pause-event", + node_id="node-1", + node_type=NodeType.CODE, + reason=SchedulingPause(message="test pause"), + ), + NodeRunSucceededEvent( + id="success-event", + node_id="node-1", + node_type=NodeType.CODE, + start_at=datetime.utcnow(), + node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED), + ), + ] + + event_queue: queue.Queue = queue.Queue() + for e in events: + event_queue.put(e) + + coordinator = _StubExecutionCoordinator() + event_handler = _PauseStubEventHandler(coordinator) + + dispatcher = Dispatcher( + event_queue=event_queue, + event_handler=event_handler, + execution_coordinator=coordinator, + ) + + dispatcher._dispatcher_loop() + + # ensure all events are drained. + assert event_queue.empty() diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_command_system.py b/api/tests/unit_tests/core/workflow/graph_engine/test_command_system.py index b29baf5a9f..868edf9832 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_command_system.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_command_system.py @@ -3,13 +3,17 @@ import time from unittest.mock import MagicMock +from core.app.entities.app_invoke_entities import InvokeFrom +from core.workflow.entities.graph_init_params import GraphInitParams from core.workflow.entities.pause_reason import SchedulingPause from core.workflow.graph import Graph from core.workflow.graph_engine import GraphEngine from core.workflow.graph_engine.command_channels import InMemoryChannel from core.workflow.graph_engine.entities.commands import AbortCommand, CommandType, PauseCommand from core.workflow.graph_events import GraphRunAbortedEvent, GraphRunPausedEvent, GraphRunStartedEvent +from core.workflow.nodes.start.start_node import StartNode from core.workflow.runtime import GraphRuntimeState, VariablePool +from models.enums import UserFrom def test_abort_command(): @@ -26,11 +30,23 @@ def test_abort_command(): mock_graph.root_node.id = "start" # Create mock nodes with required attributes - using shared runtime state - mock_start_node = MagicMock() - mock_start_node.state = None - mock_start_node.id = "start" - mock_start_node.graph_runtime_state = shared_runtime_state # Use shared instance - mock_graph.nodes["start"] = mock_start_node + start_node = StartNode( + id="start", + config={"id": "start"}, + graph_init_params=GraphInitParams( + tenant_id="test_tenant", + app_id="test_app", + workflow_id="test_workflow", + graph_config={}, + user_id="test_user", + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + call_depth=0, + ), + graph_runtime_state=shared_runtime_state, + ) + start_node.init_node_data({"title": "start", "variables": []}) + mock_graph.nodes["start"] = start_node # Mock graph methods mock_graph.get_outgoing_edges = MagicMock(return_value=[]) @@ -124,11 +140,23 @@ def test_pause_command(): mock_graph.root_node = MagicMock() mock_graph.root_node.id = "start" - mock_start_node = MagicMock() - mock_start_node.state = None - mock_start_node.id = "start" - mock_start_node.graph_runtime_state = shared_runtime_state - mock_graph.nodes["start"] = mock_start_node + start_node = StartNode( + id="start", + config={"id": "start"}, + graph_init_params=GraphInitParams( + tenant_id="test_tenant", + app_id="test_app", + workflow_id="test_workflow", + graph_config={}, + user_id="test_user", + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + call_depth=0, + ), + graph_runtime_state=shared_runtime_state, + ) + start_node.init_node_data({"title": "start", "variables": []}) + mock_graph.nodes["start"] = start_node mock_graph.get_outgoing_edges = MagicMock(return_value=[]) mock_graph.get_incoming_edges = MagicMock(return_value=[]) @@ -153,5 +181,5 @@ def test_pause_command(): assert pause_events[0].reason == SchedulingPause(message="User requested pause") graph_execution = engine.graph_runtime_state.graph_execution - assert graph_execution.is_paused + assert graph_execution.paused assert graph_execution.pause_reason == SchedulingPause(message="User requested pause") 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 deleted file mode 100644 index 3fe4ce3400..0000000000 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_dispatcher.py +++ /dev/null @@ -1,109 +0,0 @@ -"""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 - self._paused = False - - def check_commands(self) -> None: - self.command_checks += 1 - - def check_scaling(self) -> None: - self.scaling_checks += 1 - - @property - def is_paused(self) -> bool: - return self._paused - - 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_during_idle_and_on_completion() -> None: - """Dispatcher polls commands when idle and after completion events.""" - started_checks = _run_dispatcher_for_event(_make_started_event()) - succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event()) - - assert started_checks == 1 - assert succeeded_checks == 2 diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_execution_coordinator.py b/api/tests/unit_tests/core/workflow/graph_engine/test_execution_coordinator.py index 025393e435..0d67a76169 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_execution_coordinator.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_execution_coordinator.py @@ -48,15 +48,3 @@ def test_handle_pause_noop_when_execution_running() -> None: worker_pool.stop.assert_not_called() state_manager.clear_executing.assert_not_called() - - -def test_is_execution_complete_when_paused() -> None: - """Paused execution should be treated as complete.""" - graph_execution = GraphExecution(workflow_id="workflow") - graph_execution.start() - graph_execution.pause("Awaiting input") - - coordinator, state_manager, _worker_pool = _build_coordinator(graph_execution) - state_manager.is_execution_complete.return_value = False - - assert coordinator.is_execution_complete() diff --git a/docker/.env.example b/docker/.env.example index 519f4aa3e0..5cb948d835 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -365,10 +365,9 @@ WEB_API_CORS_ALLOW_ORIGINS=* # Specifies the allowed origins for cross-origin requests to the console API, # e.g. https://cloud.dify.ai or * for all origins. CONSOLE_CORS_ALLOW_ORIGINS=* -# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains. -# Provide the registrable domain (e.g. example.com); leading dots are optional. +# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). Leading dots are optional. COOKIE_DOMAIN= -# The frontend reads NEXT_PUBLIC_COOKIE_DOMAIN to align cookie handling with the API. +# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. NEXT_PUBLIC_COOKIE_DOMAIN= # ------------------------------ diff --git a/docker/tidb/docker-compose.yaml b/docker/tidb/docker-compose.yaml index fa15770175..9db6922108 100644 --- a/docker/tidb/docker-compose.yaml +++ b/docker/tidb/docker-compose.yaml @@ -55,7 +55,8 @@ services: - ./volumes/data:/data - ./volumes/logs:/logs command: - - --config=/tiflash.toml + - server + - --config-file=/tiflash.toml depends_on: - "tikv" - "tidb" diff --git a/web/.env.example b/web/.env.example index 5bfcc9dac0..eff6f77fd9 100644 --- a/web/.env.example +++ b/web/.env.example @@ -12,6 +12,9 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api # console or api domain. # example: http://udify.app/api NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api +# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. +NEXT_PUBLIC_COOKIE_DOMAIN= + # The API PREFIX for MARKETPLACE NEXT_PUBLIC_MARKETPLACE_API_PREFIX=https://marketplace.dify.ai/api/v1 # The URL for MARKETPLACE @@ -34,9 +37,6 @@ NEXT_PUBLIC_CSP_WHITELIST= # Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking NEXT_PUBLIC_ALLOW_EMBED= -# Shared cookie domain when console UI and API use different subdomains (e.g. example.com) -NEXT_PUBLIC_COOKIE_DOMAIN= - # Allow rendering unsafe URLs which have "data:" scheme. NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false diff --git a/web/README.md b/web/README.md index a47cfab041..6daf1e922e 100644 --- a/web/README.md +++ b/web/README.md @@ -32,6 +32,7 @@ NEXT_PUBLIC_EDITION=SELF_HOSTED # different from api or web app domain. # example: http://cloud.dify.ai/console/api NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api +NEXT_PUBLIC_COOKIE_DOMAIN= # The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from # console or api domain. # example: http://udify.app/api @@ -41,6 +42,11 @@ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api NEXT_PUBLIC_SENTRY_DSN= ``` +> [!IMPORTANT] +> +> 1. When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. The frontend and backend must be under the same top-level domain in order to share authentication cookies. +> 1. It's necessary to set NEXT_PUBLIC_API_PREFIX and NEXT_PUBLIC_PUBLIC_API_PREFIX to the correct backend API URL. + Finally, run the development server: ```bash diff --git a/web/i18n/de-DE/app-overview.ts b/web/i18n/de-DE/app-overview.ts index f8e934a117..9fa93d4aff 100644 --- a/web/i18n/de-DE/app-overview.ts +++ b/web/i18n/de-DE/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Abschießen', + enableTooltip: {}, }, apiInfo: { title: 'Backend-Service-API', @@ -125,6 +126,10 @@ const translation = { running: 'In Betrieb', disable: 'Deaktivieren', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Die Funktion {{feature}} wird im Trigger-Knoten-Modus nicht unterstützt.', + }, }, analysis: { title: 'Analyse', diff --git a/web/i18n/es-ES/app-overview.ts b/web/i18n/es-ES/app-overview.ts index 8413aa276a..f9e10c4b5c 100644 --- a/web/i18n/es-ES/app-overview.ts +++ b/web/i18n/es-ES/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Lanzar', + enableTooltip: {}, }, apiInfo: { title: 'API del servicio backend', @@ -125,6 +126,10 @@ const translation = { running: 'En servicio', disable: 'Deshabilitar', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'La función {{feature}} no es compatible en el modo Nodo de disparo.', + }, }, analysis: { title: 'Análisis', diff --git a/web/i18n/fa-IR/app-overview.ts b/web/i18n/fa-IR/app-overview.ts index 891002b4e4..a77077b922 100644 --- a/web/i18n/fa-IR/app-overview.ts +++ b/web/i18n/fa-IR/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'راه اندازی', + enableTooltip: {}, }, apiInfo: { title: 'API سرویس بک‌اند', @@ -125,6 +126,10 @@ const translation = { running: 'در حال سرویس‌دهی', disable: 'غیرفعال', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'ویژگی {{feature}} در حالت گره تریگر پشتیبانی نمی‌شود.', + }, }, analysis: { title: 'تحلیل', diff --git a/web/i18n/fr-FR/app-overview.ts b/web/i18n/fr-FR/app-overview.ts index 82db5d0be8..6c873c42c2 100644 --- a/web/i18n/fr-FR/app-overview.ts +++ b/web/i18n/fr-FR/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Lancer', + enableTooltip: {}, }, apiInfo: { title: 'API de service Backend', @@ -125,6 +126,10 @@ const translation = { running: 'En service', disable: 'Désactiver', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'La fonctionnalité {{feature}} n\'est pas prise en charge en mode Nœud Déclencheur.', + }, }, analysis: { title: 'Analyse', diff --git a/web/i18n/hi-IN/app-overview.ts b/web/i18n/hi-IN/app-overview.ts index 8a431e4bd9..17d383f2bc 100644 --- a/web/i18n/hi-IN/app-overview.ts +++ b/web/i18n/hi-IN/app-overview.ts @@ -125,6 +125,7 @@ const translation = { }, }, launch: 'लॉन्च', + enableTooltip: {}, }, apiInfo: { title: 'बैकएंड सेवा एपीआई', @@ -136,6 +137,10 @@ const translation = { running: 'सेवा में', disable: 'अक्षम करें', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'ट्रिगर नोड मोड में {{feature}} फ़ीचर समर्थित नहीं है।', + }, }, analysis: { title: 'विश्लेषण', diff --git a/web/i18n/id-ID/app-overview.ts b/web/i18n/id-ID/app-overview.ts index 474e85bfd5..0bd9b9e1c7 100644 --- a/web/i18n/id-ID/app-overview.ts +++ b/web/i18n/id-ID/app-overview.ts @@ -111,6 +111,7 @@ const translation = { preUseReminder: 'Harap aktifkan aplikasi web sebelum melanjutkan.', regenerateNotice: 'Apakah Anda ingin membuat ulang URL publik?', explanation: 'Aplikasi web AI siap pakai', + enableTooltip: {}, }, apiInfo: { accessibleAddress: 'Titik Akhir API Layanan', @@ -123,6 +124,10 @@ const translation = { running: 'Berjalan', }, title: 'Ikhtisar', + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Fitur {{feature}} tidak didukung dalam mode Node Pemicu.', + }, }, analysis: { totalMessages: { diff --git a/web/i18n/it-IT/app-overview.ts b/web/i18n/it-IT/app-overview.ts index 2c9a3b476f..513740e0ee 100644 --- a/web/i18n/it-IT/app-overview.ts +++ b/web/i18n/it-IT/app-overview.ts @@ -127,6 +127,7 @@ const translation = { }, }, launch: 'Lanciare', + enableTooltip: {}, }, apiInfo: { title: 'API del servizio backend', @@ -138,6 +139,10 @@ const translation = { running: 'In servizio', disable: 'Disabilita', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'La funzionalità {{feature}} non è supportata in modalità Nodo Trigger.', + }, }, analysis: { title: 'Analisi', diff --git a/web/i18n/ko-KR/app-overview.ts b/web/i18n/ko-KR/app-overview.ts index 136e472a24..9859c47af2 100644 --- a/web/i18n/ko-KR/app-overview.ts +++ b/web/i18n/ko-KR/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: '발사', + enableTooltip: {}, }, apiInfo: { title: '백엔드 서비스 API', @@ -125,6 +126,10 @@ const translation = { running: '서비스 중', disable: '비활성', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: '트리거 노드 모드에서는 {{feature}} 기능이 지원되지 않습니다.', + }, }, analysis: { title: '분석', diff --git a/web/i18n/pl-PL/app-overview.ts b/web/i18n/pl-PL/app-overview.ts index 8ac97e6277..ab0b4e24d5 100644 --- a/web/i18n/pl-PL/app-overview.ts +++ b/web/i18n/pl-PL/app-overview.ts @@ -125,6 +125,7 @@ const translation = { }, }, launch: 'Uruchomić', + enableTooltip: {}, }, apiInfo: { title: 'API usługi w tle', @@ -136,6 +137,10 @@ const translation = { running: 'W usłudze', disable: 'Wyłącz', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Funkcja {{feature}} nie jest obsługiwana w trybie węzła wyzwalającego.', + }, }, analysis: { title: 'Analiza', diff --git a/web/i18n/pt-BR/app-overview.ts b/web/i18n/pt-BR/app-overview.ts index a6a76f1cf3..37106ed15e 100644 --- a/web/i18n/pt-BR/app-overview.ts +++ b/web/i18n/pt-BR/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Lançar', + enableTooltip: {}, }, apiInfo: { title: 'API de Serviço de Back-end', @@ -125,6 +126,10 @@ const translation = { running: 'Em serviço', disable: 'Desabilitar', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'O recurso {{feature}} não é compatível no modo Nó de Gatilho.', + }, }, analysis: { title: 'Análise', diff --git a/web/i18n/ro-RO/app-overview.ts b/web/i18n/ro-RO/app-overview.ts index 04b7540ff9..00f7079ad3 100644 --- a/web/i18n/ro-RO/app-overview.ts +++ b/web/i18n/ro-RO/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Lansa', + enableTooltip: {}, }, apiInfo: { title: 'API serviciu backend', @@ -125,6 +126,10 @@ const translation = { running: 'În service', disable: 'Dezactivat', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Funcționalitatea {{feature}} nu este suportată în modul Nod Trigger.', + }, }, analysis: { title: 'Analiză', diff --git a/web/i18n/ru-RU/app-overview.ts b/web/i18n/ru-RU/app-overview.ts index ae7ec32f5a..47a411c42c 100644 --- a/web/i18n/ru-RU/app-overview.ts +++ b/web/i18n/ru-RU/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Баркас', + enableTooltip: {}, }, apiInfo: { title: 'API серверной части', @@ -125,6 +126,10 @@ const translation = { running: 'В работе', disable: 'Отключено', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Функция {{feature}} не поддерживается в режиме узла триггера.', + }, }, analysis: { title: 'Анализ', diff --git a/web/i18n/sl-SI/app-overview.ts b/web/i18n/sl-SI/app-overview.ts index 8d577300d0..11a3359021 100644 --- a/web/i18n/sl-SI/app-overview.ts +++ b/web/i18n/sl-SI/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Začetek', + enableTooltip: {}, }, apiInfo: { title: 'API storitev v ozadju', @@ -125,6 +126,10 @@ const translation = { running: 'V storitvi', disable: 'Onemogočeno', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Funkcija {{feature}} ni podprta v načinu vozlišča sprožilca.', + }, }, analysis: { title: 'Analiza', diff --git a/web/i18n/th-TH/app-overview.ts b/web/i18n/th-TH/app-overview.ts index 87eddf1f7a..e3d14ffcbd 100644 --- a/web/i18n/th-TH/app-overview.ts +++ b/web/i18n/th-TH/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'เรือยนต์', + enableTooltip: {}, }, apiInfo: { title: 'API บริการแบ็กเอนด์', @@ -125,6 +126,10 @@ const translation = { running: 'ให้บริการ', disable: 'พิการ', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'โหมดโหนดทริกเกอร์ไม่รองรับฟีเจอร์ {{feature}}.', + }, }, analysis: { title: 'การวิเคราะห์', diff --git a/web/i18n/tr-TR/app-overview.ts b/web/i18n/tr-TR/app-overview.ts index f6c16553f1..a0e79f2354 100644 --- a/web/i18n/tr-TR/app-overview.ts +++ b/web/i18n/tr-TR/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Başlat', + enableTooltip: {}, }, apiInfo: { title: 'Arka Uç Servis API\'si', @@ -125,6 +126,10 @@ const translation = { running: 'Hizmette', disable: 'Devre Dışı', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Trigger Düğümü modunda {{feature}} özelliği desteklenmiyor.', + }, }, analysis: { title: 'Analiz', diff --git a/web/i18n/uk-UA/app-overview.ts b/web/i18n/uk-UA/app-overview.ts index 1a95b47abd..a42397bb38 100644 --- a/web/i18n/uk-UA/app-overview.ts +++ b/web/i18n/uk-UA/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Запуску', + enableTooltip: {}, }, apiInfo: { title: 'API сервісу Backend', @@ -125,6 +126,10 @@ const translation = { running: 'У роботі', disable: 'Вимкнути', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Функція {{feature}} не підтримується в режимі вузла тригера.', + }, }, analysis: { title: 'Аналіз', diff --git a/web/i18n/vi-VN/app-overview.ts b/web/i18n/vi-VN/app-overview.ts index 34f3735beb..705d0bf192 100644 --- a/web/i18n/vi-VN/app-overview.ts +++ b/web/i18n/vi-VN/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: 'Phóng', + enableTooltip: {}, }, apiInfo: { title: 'API dịch vụ backend', @@ -125,6 +126,10 @@ const translation = { running: 'Đang hoạt động', disable: 'Đã tắt', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: 'Tính năng {{feature}} không được hỗ trợ trong chế độ Nút Kích hoạt.', + }, }, analysis: { title: 'Phân tích', diff --git a/web/i18n/zh-Hant/app-overview.ts b/web/i18n/zh-Hant/app-overview.ts index 21d9247361..5bd8203171 100644 --- a/web/i18n/zh-Hant/app-overview.ts +++ b/web/i18n/zh-Hant/app-overview.ts @@ -114,6 +114,7 @@ const translation = { }, }, launch: '發射', + enableTooltip: {}, }, apiInfo: { title: '後端服務 API', @@ -125,6 +126,10 @@ const translation = { running: '執行中', disable: '已停用', }, + triggerInfo: {}, + disableTooltip: { + triggerMode: '觸發節點模式不支援 {{feature}} 功能。', + }, }, analysis: { title: '分析',