mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 12:37:20 +08:00
Merge branch 'main' into refactor/drop-swr
This commit is contained in:
commit
2db7fa029c
8
.claude/settings.json
Normal file
8
.claude/settings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"feature-dev@claude-plugins-official": true,
|
||||||
|
"context7@claude-plugins-official": true,
|
||||||
|
"typescript-lsp@claude-plugins-official": true,
|
||||||
|
"pyright-lsp@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [],
|
|
||||||
"deny": []
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.",
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
},
|
|
||||||
"enabledMcpjsonServers": [
|
|
||||||
"context7",
|
|
||||||
"sequential-thinking",
|
|
||||||
"github",
|
|
||||||
"fetch",
|
|
||||||
"playwright",
|
|
||||||
"ide"
|
|
||||||
],
|
|
||||||
"enableAllProjectMcpServers": true
|
|
||||||
}
|
|
||||||
34
.mcp.json
34
.mcp.json
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"context7": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://mcp.context7.com/mcp"
|
|
||||||
},
|
|
||||||
"sequential-thinking": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
||||||
"env": {}
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
||||||
"env": {
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fetch": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "uvx",
|
|
||||||
"args": ["mcp-server-fetch"],
|
|
||||||
"env": {}
|
|
||||||
},
|
|
||||||
"playwright": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@playwright/mcp@latest"],
|
|
||||||
"env": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -90,6 +90,7 @@ class AppQueueManager:
|
|||||||
"""
|
"""
|
||||||
self._clear_task_belong_cache()
|
self._clear_task_belong_cache()
|
||||||
self._q.put(None)
|
self._q.put(None)
|
||||||
|
self._graph_runtime_state = None # Release reference to allow GC to reclaim memory
|
||||||
|
|
||||||
def _clear_task_belong_cache(self) -> None:
|
def _clear_task_belong_cache(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -313,17 +313,20 @@ class StreamableHTTPTransport:
|
|||||||
if is_initialization:
|
if is_initialization:
|
||||||
self._maybe_extract_session_id_from_response(response)
|
self._maybe_extract_session_id_from_response(response)
|
||||||
|
|
||||||
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
|
# Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:
|
||||||
|
# The server MUST NOT send a response to notifications.
|
||||||
|
if isinstance(message.root, JSONRPCRequest):
|
||||||
|
content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower())
|
||||||
|
|
||||||
if content_type.startswith(JSON):
|
if content_type.startswith(JSON):
|
||||||
self._handle_json_response(response, ctx.server_to_client_queue)
|
self._handle_json_response(response, ctx.server_to_client_queue)
|
||||||
elif content_type.startswith(SSE):
|
elif content_type.startswith(SSE):
|
||||||
self._handle_sse_response(response, ctx)
|
self._handle_sse_response(response, ctx)
|
||||||
else:
|
else:
|
||||||
self._handle_unexpected_content_type(
|
self._handle_unexpected_content_type(
|
||||||
content_type,
|
content_type,
|
||||||
ctx.server_to_client_queue,
|
ctx.server_to_client_queue,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_json_response(
|
def _handle_json_response(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -6,7 +6,15 @@ from typing import Any
|
|||||||
|
|
||||||
from core.mcp.auth_client import MCPClientWithAuthRetry
|
from core.mcp.auth_client import MCPClientWithAuthRetry
|
||||||
from core.mcp.error import MCPConnectionError
|
from core.mcp.error import MCPConnectionError
|
||||||
from core.mcp.types import AudioContent, CallToolResult, ImageContent, TextContent
|
from core.mcp.types import (
|
||||||
|
AudioContent,
|
||||||
|
BlobResourceContents,
|
||||||
|
CallToolResult,
|
||||||
|
EmbeddedResource,
|
||||||
|
ImageContent,
|
||||||
|
TextContent,
|
||||||
|
TextResourceContents,
|
||||||
|
)
|
||||||
from core.tools.__base.tool import Tool
|
from core.tools.__base.tool import Tool
|
||||||
from core.tools.__base.tool_runtime import ToolRuntime
|
from core.tools.__base.tool_runtime import ToolRuntime
|
||||||
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType
|
from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType
|
||||||
@ -53,10 +61,19 @@ class MCPTool(Tool):
|
|||||||
for content in result.content:
|
for content in result.content:
|
||||||
if isinstance(content, TextContent):
|
if isinstance(content, TextContent):
|
||||||
yield from self._process_text_content(content)
|
yield from self._process_text_content(content)
|
||||||
elif isinstance(content, ImageContent):
|
elif isinstance(content, ImageContent | AudioContent):
|
||||||
yield self._process_image_content(content)
|
yield self.create_blob_message(
|
||||||
elif isinstance(content, AudioContent):
|
blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType}
|
||||||
yield self._process_audio_content(content)
|
)
|
||||||
|
elif isinstance(content, EmbeddedResource):
|
||||||
|
resource = content.resource
|
||||||
|
if isinstance(resource, TextResourceContents):
|
||||||
|
yield self.create_text_message(resource.text)
|
||||||
|
elif isinstance(resource, BlobResourceContents):
|
||||||
|
mime_type = resource.mimeType or "application/octet-stream"
|
||||||
|
yield self.create_blob_message(blob=base64.b64decode(resource.blob), meta={"mime_type": mime_type})
|
||||||
|
else:
|
||||||
|
raise ToolInvokeError(f"Unsupported embedded resource type: {type(resource)}")
|
||||||
else:
|
else:
|
||||||
logger.warning("Unsupported content type=%s", type(content))
|
logger.warning("Unsupported content type=%s", type(content))
|
||||||
|
|
||||||
@ -101,14 +118,6 @@ class MCPTool(Tool):
|
|||||||
for item in json_list:
|
for item in json_list:
|
||||||
yield self.create_json_message(item)
|
yield self.create_json_message(item)
|
||||||
|
|
||||||
def _process_image_content(self, content: ImageContent) -> ToolInvokeMessage:
|
|
||||||
"""Process image content and return a blob message."""
|
|
||||||
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
|
|
||||||
|
|
||||||
def _process_audio_content(self, content: AudioContent) -> ToolInvokeMessage:
|
|
||||||
"""Process audio content and return a blob message."""
|
|
||||||
return self.create_blob_message(blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType})
|
|
||||||
|
|
||||||
def fork_tool_runtime(self, runtime: ToolRuntime) -> "MCPTool":
|
def fork_tool_runtime(self, runtime: ToolRuntime) -> "MCPTool":
|
||||||
return MCPTool(
|
return MCPTool(
|
||||||
entity=self.entity,
|
entity=self.entity,
|
||||||
|
|||||||
@ -14,7 +14,8 @@ from enums.quota_type import QuotaType, unlimited
|
|||||||
from extensions.otel import AppGenerateHandler, trace_span
|
from extensions.otel import AppGenerateHandler, trace_span
|
||||||
from models.model import Account, App, AppMode, EndUser
|
from models.model import Account, App, AppMode, EndUser
|
||||||
from models.workflow import Workflow
|
from models.workflow import Workflow
|
||||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
from services.workflow_service import WorkflowService
|
from services.workflow_service import WorkflowService
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ from models.model import App, EndUser
|
|||||||
from models.trigger import WorkflowTriggerLog
|
from models.trigger import WorkflowTriggerLog
|
||||||
from models.workflow import Workflow
|
from models.workflow import Workflow
|
||||||
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository
|
||||||
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowNotFoundError
|
from services.errors.app import QuotaExceededError, WorkflowNotFoundError, WorkflowQuotaLimitError
|
||||||
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
|
from services.workflow.entities import AsyncTriggerResponse, TriggerData, WorkflowTaskData
|
||||||
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
|
from services.workflow.queue_dispatcher import QueueDispatcherManager, QueuePriority
|
||||||
from services.workflow_service import WorkflowService
|
from services.workflow_service import WorkflowService
|
||||||
@ -141,7 +141,7 @@ class AsyncWorkflowService:
|
|||||||
trigger_log_repo.update(trigger_log)
|
trigger_log_repo.update(trigger_log)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
raise InvokeRateLimitError(
|
raise WorkflowQuotaLimitError(
|
||||||
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
|
f"Workflow execution quota limit reached for tenant {trigger_data.tenant_id}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,8 @@ class WorkflowIdFormatError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvokeRateLimitError(Exception):
|
class WorkflowQuotaLimitError(Exception):
|
||||||
"""Raised when rate limit is exceeded for workflow invocations."""
|
"""Raised when workflow execution quota is exceeded (for async/background workflows)."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -146,7 +146,7 @@ class PluginParameterService:
|
|||||||
provider,
|
provider,
|
||||||
action,
|
action,
|
||||||
resolved_credentials,
|
resolved_credentials,
|
||||||
CredentialType.API_KEY.value,
|
original_subscription.credential_type or CredentialType.UNAUTHORIZED.value,
|
||||||
parameter,
|
parameter,
|
||||||
)
|
)
|
||||||
.options
|
.options
|
||||||
|
|||||||
@ -868,48 +868,111 @@ class TriggerProviderService:
|
|||||||
if not provider_controller:
|
if not provider_controller:
|
||||||
raise ValueError(f"Provider {provider_id} not found")
|
raise ValueError(f"Provider {provider_id} not found")
|
||||||
|
|
||||||
subscription = TriggerProviderService.get_subscription_by_id(
|
# Use distributed lock to prevent race conditions on the same subscription
|
||||||
tenant_id=tenant_id,
|
lock_key = f"trigger_subscription_rebuild_lock:{tenant_id}_{subscription_id}"
|
||||||
subscription_id=subscription_id,
|
with redis_client.lock(lock_key, timeout=20):
|
||||||
)
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
if not subscription:
|
try:
|
||||||
raise ValueError(f"Subscription {subscription_id} not found")
|
# Get subscription within the transaction
|
||||||
|
subscription: TriggerSubscription | None = (
|
||||||
|
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
|
||||||
|
)
|
||||||
|
if not subscription:
|
||||||
|
raise ValueError(f"Subscription {subscription_id} not found")
|
||||||
|
|
||||||
credential_type = CredentialType.of(subscription.credential_type)
|
credential_type = CredentialType.of(subscription.credential_type)
|
||||||
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
||||||
raise ValueError("Credential type not supported for rebuild")
|
raise ValueError("Credential type not supported for rebuild")
|
||||||
|
|
||||||
# TODO: Trying to invoke update api of the plugin trigger provider
|
# Decrypt existing credentials for merging
|
||||||
|
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
controller=provider_controller,
|
||||||
|
subscription=subscription,
|
||||||
|
)
|
||||||
|
decrypted_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
|
||||||
|
|
||||||
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
|
# Merge credentials: if caller passed HIDDEN_VALUE, retain existing decrypted value
|
||||||
|
merged_credentials: dict[str, Any] = {
|
||||||
|
key: value if value != HIDDEN_VALUE else decrypted_credentials.get(key, UNKNOWN_VALUE)
|
||||||
|
for key, value in credentials.items()
|
||||||
|
}
|
||||||
|
|
||||||
# Delete the previous subscription
|
user_id = subscription.user_id
|
||||||
user_id = subscription.user_id
|
|
||||||
TriggerManager.unsubscribe_trigger(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
user_id=user_id,
|
|
||||||
provider_id=provider_id,
|
|
||||||
subscription=subscription.to_entity(),
|
|
||||||
credentials=subscription.credentials,
|
|
||||||
credential_type=credential_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new subscription with the same subscription_id and endpoint_id
|
# TODO: Trying to invoke update api of the plugin trigger provider
|
||||||
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
|
||||||
tenant_id=tenant_id,
|
# FALLBACK: If the update api is not implemented,
|
||||||
user_id=user_id,
|
# delete the previous subscription and create a new one
|
||||||
provider_id=provider_id,
|
|
||||||
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
# Unsubscribe the previous subscription (external call, but we'll handle errors)
|
||||||
parameters=parameters,
|
try:
|
||||||
credentials=credentials,
|
TriggerManager.unsubscribe_trigger(
|
||||||
credential_type=credential_type,
|
tenant_id=tenant_id,
|
||||||
)
|
user_id=user_id,
|
||||||
TriggerProviderService.update_trigger_subscription(
|
provider_id=provider_id,
|
||||||
tenant_id=tenant_id,
|
subscription=subscription.to_entity(),
|
||||||
subscription_id=subscription.id,
|
credentials=decrypted_credentials,
|
||||||
name=name,
|
credential_type=credential_type,
|
||||||
parameters=parameters,
|
)
|
||||||
credentials=credentials,
|
except Exception as e:
|
||||||
properties=new_subscription.properties,
|
logger.exception("Error unsubscribing trigger during rebuild", exc_info=e)
|
||||||
expires_at=new_subscription.expires_at,
|
# Continue anyway - the subscription might already be deleted externally
|
||||||
)
|
|
||||||
|
# Create a new subscription with the same subscription_id and endpoint_id (external call)
|
||||||
|
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
||||||
|
parameters=parameters,
|
||||||
|
credentials=merged_credentials,
|
||||||
|
credential_type=credential_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the subscription in the same transaction
|
||||||
|
# Inline update logic to reuse the same session
|
||||||
|
if name is not None and name != subscription.name:
|
||||||
|
existing = (
|
||||||
|
session.query(TriggerSubscription)
|
||||||
|
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing and existing.id != subscription.id:
|
||||||
|
raise ValueError(f"Subscription name '{name}' already exists for this provider")
|
||||||
|
subscription.name = name
|
||||||
|
|
||||||
|
# Update parameters
|
||||||
|
subscription.parameters = dict(parameters)
|
||||||
|
|
||||||
|
# Update credentials with merged (and encrypted) values
|
||||||
|
subscription.credentials = dict(credential_encrypter.encrypt(merged_credentials))
|
||||||
|
|
||||||
|
# Update properties
|
||||||
|
if new_subscription.properties:
|
||||||
|
properties_encrypter, _ = create_provider_encrypter(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
config=provider_controller.get_properties_schema(),
|
||||||
|
cache=NoOpProviderCredentialCache(),
|
||||||
|
)
|
||||||
|
subscription.properties = dict(properties_encrypter.encrypt(dict(new_subscription.properties)))
|
||||||
|
|
||||||
|
# Update expiration timestamp
|
||||||
|
if new_subscription.expires_at is not None:
|
||||||
|
subscription.expires_at = new_subscription.expires_at
|
||||||
|
|
||||||
|
# Commit the transaction
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Clear subscription cache
|
||||||
|
delete_cache_for_subscription(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
provider_id=subscription.provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Rollback on any error
|
||||||
|
session.rollback()
|
||||||
|
logger.exception("Failed to rebuild trigger subscription", exc_info=e)
|
||||||
|
raise
|
||||||
|
|||||||
@ -863,10 +863,18 @@ class WebhookService:
|
|||||||
not_found_in_cache.append(node_id)
|
not_found_in_cache.append(node_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with Session(db.engine) as session:
|
lock_key = f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock"
|
||||||
try:
|
lock = redis_client.lock(lock_key, timeout=10)
|
||||||
# lock the concurrent webhook trigger creation
|
lock_acquired = False
|
||||||
redis_client.lock(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock", timeout=10)
|
|
||||||
|
try:
|
||||||
|
# acquire the lock with blocking and timeout
|
||||||
|
lock_acquired = lock.acquire(blocking=True, blocking_timeout=10)
|
||||||
|
if not lock_acquired:
|
||||||
|
logger.warning("Failed to acquire lock for webhook sync, app %s", app.id)
|
||||||
|
raise RuntimeError("Failed to acquire lock for webhook trigger synchronization")
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
# fetch the non-cached nodes from DB
|
# fetch the non-cached nodes from DB
|
||||||
all_records = session.scalars(
|
all_records = session.scalars(
|
||||||
select(WorkflowWebhookTrigger).where(
|
select(WorkflowWebhookTrigger).where(
|
||||||
@ -903,11 +911,16 @@ class WebhookService:
|
|||||||
session.delete(nodes_id_in_db[node_id])
|
session.delete(nodes_id_in_db[node_id])
|
||||||
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:{node_id}")
|
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:{node_id}")
|
||||||
session.commit()
|
session.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to sync webhook relationships for app %s", app.id)
|
logger.exception("Failed to sync webhook relationships for app %s", app.id)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
redis_client.delete(f"{cls.__WEBHOOK_NODE_CACHE_KEY__}:apps:{app.id}:lock")
|
# release the lock only if it was acquired
|
||||||
|
if lock_acquired:
|
||||||
|
try:
|
||||||
|
lock.release()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to release lock for webhook sync, app %s", app.id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_webhook_id(cls) -> str:
|
def generate_webhook_id(cls) -> str:
|
||||||
|
|||||||
@ -0,0 +1,682 @@
|
|||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
|
||||||
|
from core.plugin.entities.plugin_daemon import CredentialType
|
||||||
|
from core.trigger.entities.entities import Subscription as TriggerSubscriptionEntity
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.provider_ids import TriggerProviderID
|
||||||
|
from models.trigger import TriggerSubscription
|
||||||
|
from services.trigger.trigger_provider_service import TriggerProviderService
|
||||||
|
|
||||||
|
|
||||||
|
class TestTriggerProviderService:
|
||||||
|
"""Integration tests for TriggerProviderService using testcontainers."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_external_service_dependencies(self):
|
||||||
|
"""Mock setup for external service dependencies."""
|
||||||
|
with (
|
||||||
|
patch("services.trigger.trigger_provider_service.TriggerManager") as mock_trigger_manager,
|
||||||
|
patch("services.trigger.trigger_provider_service.redis_client") as mock_redis_client,
|
||||||
|
patch("services.trigger.trigger_provider_service.delete_cache_for_subscription") as mock_delete_cache,
|
||||||
|
patch("services.account_service.FeatureService") as mock_account_feature_service,
|
||||||
|
):
|
||||||
|
# Setup default mock returns
|
||||||
|
mock_provider_controller = MagicMock()
|
||||||
|
mock_provider_controller.get_credential_schema_config.return_value = MagicMock()
|
||||||
|
mock_provider_controller.get_properties_schema.return_value = MagicMock()
|
||||||
|
mock_trigger_manager.get_trigger_provider.return_value = mock_provider_controller
|
||||||
|
|
||||||
|
# Mock redis lock
|
||||||
|
mock_lock = MagicMock()
|
||||||
|
mock_lock.__enter__ = MagicMock(return_value=None)
|
||||||
|
mock_lock.__exit__ = MagicMock(return_value=None)
|
||||||
|
mock_redis_client.lock.return_value = mock_lock
|
||||||
|
|
||||||
|
# Setup account feature service mock
|
||||||
|
mock_account_feature_service.get_system_features.return_value.is_allow_register = True
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"trigger_manager": mock_trigger_manager,
|
||||||
|
"redis_client": mock_redis_client,
|
||||||
|
"delete_cache": mock_delete_cache,
|
||||||
|
"provider_controller": mock_provider_controller,
|
||||||
|
"account_feature_service": mock_account_feature_service,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_test_account_and_tenant(self, db_session_with_containers, mock_external_service_dependencies):
|
||||||
|
"""
|
||||||
|
Helper method to create a test account and tenant for testing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
mock_external_service_dependencies: Mock dependencies
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (account, tenant) - Created account and tenant instances
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
from services.account_service import AccountService, TenantService
|
||||||
|
|
||||||
|
# Setup mocks for account creation
|
||||||
|
mock_external_service_dependencies[
|
||||||
|
"account_feature_service"
|
||||||
|
].get_system_features.return_value.is_allow_register = True
|
||||||
|
mock_external_service_dependencies[
|
||||||
|
"trigger_manager"
|
||||||
|
].get_trigger_provider.return_value = mock_external_service_dependencies["provider_controller"]
|
||||||
|
|
||||||
|
# Create account and tenant
|
||||||
|
account = AccountService.create_account(
|
||||||
|
email=fake.email(),
|
||||||
|
name=fake.name(),
|
||||||
|
interface_language="en-US",
|
||||||
|
password=fake.password(length=12),
|
||||||
|
)
|
||||||
|
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company())
|
||||||
|
tenant = account.current_tenant
|
||||||
|
|
||||||
|
return account, tenant
|
||||||
|
|
||||||
|
def _create_test_subscription(
|
||||||
|
self,
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper method to create a test trigger subscription.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session
|
||||||
|
tenant_id: Tenant ID
|
||||||
|
user_id: User ID
|
||||||
|
provider_id: Provider ID
|
||||||
|
credential_type: Credential type
|
||||||
|
credentials: Credentials dict
|
||||||
|
mock_external_service_dependencies: Mock dependencies
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TriggerSubscription: Created subscription instance
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||||
|
from core.helper.provider_encryption import create_provider_encrypter
|
||||||
|
|
||||||
|
# Use mock provider controller to encrypt credentials
|
||||||
|
provider_controller = mock_external_service_dependencies["provider_controller"]
|
||||||
|
|
||||||
|
# Create encrypter for credentials
|
||||||
|
credential_encrypter, _ = create_provider_encrypter(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
config=provider_controller.get_credential_schema_config(credential_type),
|
||||||
|
cache=NoOpProviderCredentialCache(),
|
||||||
|
)
|
||||||
|
|
||||||
|
subscription = TriggerSubscription(
|
||||||
|
name=fake.word(),
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
provider_id=str(provider_id),
|
||||||
|
endpoint_id=fake.uuid4(),
|
||||||
|
parameters={"param1": "value1"},
|
||||||
|
properties={"prop1": "value1"},
|
||||||
|
credentials=dict(credential_encrypter.encrypt(credentials)),
|
||||||
|
credential_type=credential_type.value,
|
||||||
|
credential_expires_at=-1,
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(subscription)
|
||||||
|
db.session.commit()
|
||||||
|
db.session.refresh(subscription)
|
||||||
|
|
||||||
|
return subscription
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_success_with_merged_credentials(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test successful rebuild with credential merging (HIDDEN_VALUE handling).
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Credentials are properly merged (HIDDEN_VALUE replaced with existing values)
|
||||||
|
- Single transaction wraps all operations
|
||||||
|
- Merged credentials are used for subscribe and update
|
||||||
|
- Database state is correctly updated
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
# Create initial subscription with credentials
|
||||||
|
original_credentials = {"api_key": "original-secret-key", "api_secret": "original-secret"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare new credentials with HIDDEN_VALUE for api_key (should keep original)
|
||||||
|
# and new value for api_secret (should update)
|
||||||
|
new_credentials = {
|
||||||
|
"api_key": HIDDEN_VALUE, # Should be replaced with original
|
||||||
|
"api_secret": "new-secret-value", # Should be updated
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock subscribe_trigger to return a new subscription entity
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription.endpoint_id,
|
||||||
|
parameters={"param1": "value1"},
|
||||||
|
properties={"prop1": "new_prop_value"},
|
||||||
|
expires_at=1234567890,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
|
||||||
|
# Mock unsubscribe_trigger
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Execute rebuild
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials=new_credentials,
|
||||||
|
parameters={"param1": "updated_value"},
|
||||||
|
name="updated_name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify unsubscribe was called with decrypted original credentials
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.assert_called_once()
|
||||||
|
unsubscribe_call_args = mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.call_args
|
||||||
|
assert unsubscribe_call_args.kwargs["tenant_id"] == tenant.id
|
||||||
|
assert unsubscribe_call_args.kwargs["provider_id"] == provider_id
|
||||||
|
assert unsubscribe_call_args.kwargs["credential_type"] == credential_type
|
||||||
|
|
||||||
|
# Verify subscribe was called with merged credentials (api_key from original, api_secret new)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
|
||||||
|
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||||
|
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||||
|
assert subscribe_credentials["api_key"] == original_credentials["api_key"] # Merged from original
|
||||||
|
assert subscribe_credentials["api_secret"] == "new-secret-value" # New value
|
||||||
|
|
||||||
|
# Verify database state was updated
|
||||||
|
db.session.refresh(subscription)
|
||||||
|
assert subscription.name == "updated_name"
|
||||||
|
assert subscription.parameters == {"param1": "updated_value"}
|
||||||
|
|
||||||
|
# Verify credentials in DB were updated with merged values (decrypt to check)
|
||||||
|
from core.helper.provider_cache import NoOpProviderCredentialCache
|
||||||
|
from core.helper.provider_encryption import create_provider_encrypter
|
||||||
|
|
||||||
|
# Use mock provider controller to decrypt credentials
|
||||||
|
provider_controller = mock_external_service_dependencies["provider_controller"]
|
||||||
|
credential_encrypter, _ = create_provider_encrypter(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
config=provider_controller.get_credential_schema_config(credential_type),
|
||||||
|
cache=NoOpProviderCredentialCache(),
|
||||||
|
)
|
||||||
|
decrypted_db_credentials = dict(credential_encrypter.decrypt(subscription.credentials))
|
||||||
|
assert decrypted_db_credentials["api_key"] == original_credentials["api_key"]
|
||||||
|
assert decrypted_db_credentials["api_secret"] == "new-secret-value"
|
||||||
|
|
||||||
|
# Verify cache was cleared
|
||||||
|
mock_external_service_dependencies["delete_cache"].assert_called_once_with(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=subscription.provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_with_all_new_credentials(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test rebuild when all credentials are new (no HIDDEN_VALUE).
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- All new credentials are used when no HIDDEN_VALUE is present
|
||||||
|
- Merged credentials contain only new values
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
# Create initial subscription
|
||||||
|
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# All new credentials (no HIDDEN_VALUE)
|
||||||
|
new_credentials = {
|
||||||
|
"api_key": "completely-new-key",
|
||||||
|
"api_secret": "completely-new-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription.endpoint_id,
|
||||||
|
parameters={},
|
||||||
|
properties={},
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Execute rebuild
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials=new_credentials,
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify subscribe was called with all new credentials
|
||||||
|
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||||
|
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||||
|
assert subscribe_credentials["api_key"] == "completely-new-key"
|
||||||
|
assert subscribe_credentials["api_secret"] == "completely-new-secret"
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_with_all_hidden_values(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test rebuild when all credentials are HIDDEN_VALUE (preserve all existing).
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- All HIDDEN_VALUE credentials are replaced with existing values
|
||||||
|
- Original credentials are preserved
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
original_credentials = {"api_key": "original-key", "api_secret": "original-secret"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# All HIDDEN_VALUE (should preserve all original)
|
||||||
|
new_credentials = {
|
||||||
|
"api_key": HIDDEN_VALUE,
|
||||||
|
"api_secret": HIDDEN_VALUE,
|
||||||
|
}
|
||||||
|
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription.endpoint_id,
|
||||||
|
parameters={},
|
||||||
|
properties={},
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Execute rebuild
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials=new_credentials,
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify subscribe was called with all original credentials
|
||||||
|
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||||
|
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||||
|
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
|
||||||
|
assert subscribe_credentials["api_secret"] == original_credentials["api_secret"]
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_with_missing_key_uses_unknown_value(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test rebuild when HIDDEN_VALUE is used for a key that doesn't exist in original.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- UNKNOWN_VALUE is used when HIDDEN_VALUE key doesn't exist in original credentials
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
# Original has only api_key
|
||||||
|
original_credentials = {"api_key": "original-key"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# HIDDEN_VALUE for non-existent key should use UNKNOWN_VALUE
|
||||||
|
new_credentials = {
|
||||||
|
"api_key": HIDDEN_VALUE,
|
||||||
|
"non_existent_key": HIDDEN_VALUE, # This key doesn't exist in original
|
||||||
|
}
|
||||||
|
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription.endpoint_id,
|
||||||
|
parameters={},
|
||||||
|
properties={},
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Execute rebuild
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials=new_credentials,
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify subscribe was called with original api_key and UNKNOWN_VALUE for missing key
|
||||||
|
subscribe_call_args = mock_external_service_dependencies["trigger_manager"].subscribe_trigger.call_args
|
||||||
|
subscribe_credentials = subscribe_call_args.kwargs["credentials"]
|
||||||
|
assert subscribe_credentials["api_key"] == original_credentials["api_key"]
|
||||||
|
assert subscribe_credentials["non_existent_key"] == UNKNOWN_VALUE
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_rollback_on_error(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test that transaction is rolled back on error.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Database transaction is rolled back when an error occurs
|
||||||
|
- Original subscription state is preserved
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
original_credentials = {"api_key": "original-key"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
original_name = subscription.name
|
||||||
|
original_parameters = subscription.parameters.copy()
|
||||||
|
|
||||||
|
# Make subscribe_trigger raise an error
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.side_effect = ValueError(
|
||||||
|
"Subscribe failed"
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Execute rebuild and expect error
|
||||||
|
with pytest.raises(ValueError, match="Subscribe failed"):
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials={"api_key": "new-key"},
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify subscription state was not changed (rolled back)
|
||||||
|
db.session.refresh(subscription)
|
||||||
|
assert subscription.name == original_name
|
||||||
|
assert subscription.parameters == original_parameters
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_unsubscribe_error_continues(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test that unsubscribe errors are handled gracefully and operation continues.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Unsubscribe errors are caught and logged but don't stop the rebuild
|
||||||
|
- Rebuild continues even if unsubscribe fails
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
original_credentials = {"api_key": "original-key"}
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
original_credentials,
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make unsubscribe_trigger raise an error (should be caught and continue)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.side_effect = ValueError(
|
||||||
|
"Unsubscribe failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription.endpoint_id,
|
||||||
|
parameters={},
|
||||||
|
properties={},
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
|
||||||
|
# Execute rebuild - should succeed despite unsubscribe error
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials={"api_key": "new-key"},
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify subscribe was still called (operation continued)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.assert_called_once()
|
||||||
|
|
||||||
|
# Verify subscription was updated
|
||||||
|
db.session.refresh(subscription)
|
||||||
|
assert subscription.parameters == {}
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_subscription_not_found(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test error when subscription is not found.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Proper error is raised when subscription doesn't exist
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
fake_subscription_id = fake.uuid4()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=fake_subscription_id,
|
||||||
|
credentials={},
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_provider_not_found(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test error when provider is not found.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Proper error is raised when provider doesn't exist
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("non_existent_org/non_existent_plugin/non_existent_provider")
|
||||||
|
|
||||||
|
# Make get_trigger_provider return None
|
||||||
|
mock_external_service_dependencies["trigger_manager"].get_trigger_provider.return_value = None
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Provider.*not found"):
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=fake.uuid4(),
|
||||||
|
credentials={},
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_unsupported_credential_type(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test error when credential type is not supported for rebuild.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Proper error is raised for unsupported credential types (not OAUTH2 or API_KEY)
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.UNAUTHORIZED # Not supported
|
||||||
|
|
||||||
|
subscription = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
{},
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Credential type not supported for rebuild"):
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
credentials={},
|
||||||
|
parameters={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rebuild_trigger_subscription_name_uniqueness_check(
|
||||||
|
self, db_session_with_containers, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test that name uniqueness is checked when updating name.
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
- Error is raised when new name conflicts with existing subscription
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account, tenant = self._create_test_account_and_tenant(
|
||||||
|
db_session_with_containers, mock_external_service_dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID("test_org/test_plugin/test_provider")
|
||||||
|
credential_type = CredentialType.API_KEY
|
||||||
|
|
||||||
|
# Create first subscription
|
||||||
|
subscription1 = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
{"api_key": "key1"},
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create second subscription with different name
|
||||||
|
subscription2 = self._create_test_subscription(
|
||||||
|
db_session_with_containers,
|
||||||
|
tenant.id,
|
||||||
|
account.id,
|
||||||
|
provider_id,
|
||||||
|
credential_type,
|
||||||
|
{"api_key": "key2"},
|
||||||
|
mock_external_service_dependencies,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_subscription_entity = TriggerSubscriptionEntity(
|
||||||
|
endpoint=subscription2.endpoint_id,
|
||||||
|
parameters={},
|
||||||
|
properties={},
|
||||||
|
expires_at=-1,
|
||||||
|
)
|
||||||
|
mock_external_service_dependencies["trigger_manager"].subscribe_trigger.return_value = new_subscription_entity
|
||||||
|
mock_external_service_dependencies["trigger_manager"].unsubscribe_trigger.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Try to rename subscription2 to subscription1's name (should fail)
|
||||||
|
with pytest.raises(ValueError, match="already exists"):
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription2.id,
|
||||||
|
credentials={"api_key": "new-key"},
|
||||||
|
parameters={},
|
||||||
|
name=subscription1.name, # Conflicting name
|
||||||
|
)
|
||||||
122
api/tests/unit_tests/tools/test_mcp_tool.py
Normal file
122
api/tests/unit_tests/tools/test_mcp_tool.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import base64
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.mcp.types import (
|
||||||
|
AudioContent,
|
||||||
|
BlobResourceContents,
|
||||||
|
CallToolResult,
|
||||||
|
EmbeddedResource,
|
||||||
|
ImageContent,
|
||||||
|
TextResourceContents,
|
||||||
|
)
|
||||||
|
from core.tools.__base.tool_runtime import ToolRuntime
|
||||||
|
from core.tools.entities.common_entities import I18nObject
|
||||||
|
from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage
|
||||||
|
from core.tools.mcp_tool.tool import MCPTool
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mcp_tool(output_schema: dict | None = None) -> MCPTool:
|
||||||
|
identity = ToolIdentity(
|
||||||
|
author="test",
|
||||||
|
name="test_mcp_tool",
|
||||||
|
label=I18nObject(en_US="Test MCP Tool", zh_Hans="测试MCP工具"),
|
||||||
|
provider="test_provider",
|
||||||
|
)
|
||||||
|
entity = ToolEntity(identity=identity, output_schema=output_schema or {})
|
||||||
|
runtime = Mock(spec=ToolRuntime)
|
||||||
|
runtime.credentials = {}
|
||||||
|
return MCPTool(
|
||||||
|
entity=entity,
|
||||||
|
runtime=runtime,
|
||||||
|
tenant_id="test_tenant",
|
||||||
|
icon="",
|
||||||
|
server_url="https://server.invalid",
|
||||||
|
provider_id="provider_1",
|
||||||
|
headers={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMCPToolInvoke:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("content_factory", "mime_type"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
lambda b64, mt: ImageContent(type="image", data=b64, mimeType=mt),
|
||||||
|
"image/png",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
lambda b64, mt: AudioContent(type="audio", data=b64, mimeType=mt),
|
||||||
|
"audio/mpeg",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_invoke_image_or_audio_yields_blob(self, content_factory, mime_type) -> None:
|
||||||
|
tool = _make_mcp_tool()
|
||||||
|
raw = b"\x00\x01test-bytes\x02"
|
||||||
|
b64 = base64.b64encode(raw).decode()
|
||||||
|
content = content_factory(b64, mime_type)
|
||||||
|
result = CallToolResult(content=[content])
|
||||||
|
|
||||||
|
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||||
|
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||||
|
|
||||||
|
assert len(messages) == 1
|
||||||
|
msg = messages[0]
|
||||||
|
assert msg.type == ToolInvokeMessage.MessageType.BLOB
|
||||||
|
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
|
||||||
|
assert msg.message.blob == raw
|
||||||
|
assert msg.meta == {"mime_type": mime_type}
|
||||||
|
|
||||||
|
def test_invoke_embedded_text_resource_yields_text(self) -> None:
|
||||||
|
tool = _make_mcp_tool()
|
||||||
|
text_resource = TextResourceContents(uri="file://test.txt", mimeType="text/plain", text="hello world")
|
||||||
|
content = EmbeddedResource(type="resource", resource=text_resource)
|
||||||
|
result = CallToolResult(content=[content])
|
||||||
|
|
||||||
|
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||||
|
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||||
|
|
||||||
|
assert len(messages) == 1
|
||||||
|
msg = messages[0]
|
||||||
|
assert msg.type == ToolInvokeMessage.MessageType.TEXT
|
||||||
|
assert isinstance(msg.message, ToolInvokeMessage.TextMessage)
|
||||||
|
assert msg.message.text == "hello world"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("mime_type", "expected_mime"),
|
||||||
|
[("application/pdf", "application/pdf"), (None, "application/octet-stream")],
|
||||||
|
)
|
||||||
|
def test_invoke_embedded_blob_resource_yields_blob(self, mime_type, expected_mime) -> None:
|
||||||
|
tool = _make_mcp_tool()
|
||||||
|
raw = b"binary-data"
|
||||||
|
b64 = base64.b64encode(raw).decode()
|
||||||
|
blob_resource = BlobResourceContents(uri="file://doc.bin", mimeType=mime_type, blob=b64)
|
||||||
|
content = EmbeddedResource(type="resource", resource=blob_resource)
|
||||||
|
result = CallToolResult(content=[content])
|
||||||
|
|
||||||
|
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||||
|
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||||
|
|
||||||
|
assert len(messages) == 1
|
||||||
|
msg = messages[0]
|
||||||
|
assert msg.type == ToolInvokeMessage.MessageType.BLOB
|
||||||
|
assert isinstance(msg.message, ToolInvokeMessage.BlobMessage)
|
||||||
|
assert msg.message.blob == raw
|
||||||
|
assert msg.meta == {"mime_type": expected_mime}
|
||||||
|
|
||||||
|
def test_invoke_yields_variables_when_structured_content_and_schema(self) -> None:
|
||||||
|
tool = _make_mcp_tool(output_schema={"type": "object"})
|
||||||
|
result = CallToolResult(content=[], structuredContent={"a": 1, "b": "x"})
|
||||||
|
|
||||||
|
with patch.object(tool, "invoke_remote_mcp_tool", return_value=result):
|
||||||
|
messages = list(tool._invoke(user_id="test_user", tool_parameters={}))
|
||||||
|
|
||||||
|
# Expect two variable messages corresponding to keys a and b
|
||||||
|
assert len(messages) == 2
|
||||||
|
var_msgs = [m for m in messages if isinstance(m.message, ToolInvokeMessage.VariableMessage)]
|
||||||
|
assert {m.message.variable_name for m in var_msgs} == {"a", "b"}
|
||||||
|
# Validate values
|
||||||
|
values = {m.message.variable_name: m.message.variable_value for m in var_msgs}
|
||||||
|
assert values == {"a": 1, "b": "x"}
|
||||||
6
api/uv.lock
generated
6
api/uv.lock
generated
@ -3072,11 +3072,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "json-repair"
|
name = "json-repair"
|
||||||
version = "0.54.1"
|
version = "0.54.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/00/46/d3a4d9a3dad39bb4a2ad16b8adb9fe2e8611b20b71197fe33daa6768e85d/json_repair-0.54.1.tar.gz", hash = "sha256:d010bc31f1fc66e7c36dc33bff5f8902674498ae5cb8e801ad455a53b455ad1d", size = 38555, upload-time = "2025-11-19T14:55:24.265Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/86/48b12ac02032f121ac7e5f11a32143edca6c1e3d19ffc54d6fb9ca0aafd0/json_repair-0.54.3.tar.gz", hash = "sha256:e50feec9725e52ac91f12184609754684ac1656119dfbd31de09bdaf9a1d8bf6", size = 38626, upload-time = "2025-12-15T09:41:58.594Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/96/c9aad7ee949cc1bf15df91f347fbc2d3bd10b30b80c7df689ce6fe9332b5/json_repair-0.54.1-py3-none-any.whl", hash = "sha256:016160c5db5d5fe443164927bb58d2dfbba5f43ad85719fa9bc51c713a443ab1", size = 29311, upload-time = "2025-11-19T14:55:22.886Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/08/abe317237add63c3e62f18a981bccf92112b431835b43d844aedaf61f4a0/json_repair-0.54.3-py3-none-any.whl", hash = "sha256:4cdc132ee27d4780576f71bf27a113877046224a808bfc17392e079cb344fb81", size = 29357, upload-time = "2025-12-15T09:41:57.436Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -54,17 +54,17 @@
|
|||||||
"publish:npm": "./scripts/publish.sh"
|
"publish:npm": "./scripts/publish.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.3.5"
|
"axios": "^1.13.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.2.0",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^25.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||||
"@typescript-eslint/parser": "^8.50.1",
|
"@typescript-eslint/parser": "^8.50.1",
|
||||||
"@vitest/coverage-v8": "1.6.1",
|
"@vitest/coverage-v8": "4.0.16",
|
||||||
"eslint": "^9.2.0",
|
"eslint": "^9.39.2",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^1.5.0"
|
"vitest": "^4.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
877
sdks/nodejs-client/pnpm-lock.yaml
generated
877
sdks/nodejs-client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
sdks/nodejs-client/pnpm-workspace.yaml
Normal file
2
sdks/nodejs-client/pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
@ -47,6 +47,12 @@ const getCheckboxDefaultSelectValue = (value: InputVar['default']) => {
|
|||||||
const parseCheckboxSelectValue = (value: string) =>
|
const parseCheckboxSelectValue = (value: string) =>
|
||||||
value === CHECKBOX_DEFAULT_TRUE_VALUE
|
value === CHECKBOX_DEFAULT_TRUE_VALUE
|
||||||
|
|
||||||
|
const normalizeSelectDefaultValue = (inputVar: InputVar) => {
|
||||||
|
if (inputVar.type === InputVarType.select && inputVar.default === '')
|
||||||
|
return { ...inputVar, default: undefined }
|
||||||
|
return inputVar
|
||||||
|
}
|
||||||
|
|
||||||
export type IConfigModalProps = {
|
export type IConfigModalProps = {
|
||||||
isCreate?: boolean
|
isCreate?: boolean
|
||||||
payload?: InputVar
|
payload?: InputVar
|
||||||
@ -67,7 +73,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { modelConfig } = useContext(ConfigContext)
|
const { modelConfig } = useContext(ConfigContext)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [tempPayload, setTempPayload] = useState<InputVar>(() => payload || getNewVarInWorkflow('') as any)
|
const [tempPayload, setTempPayload] = useState<InputVar>(() => normalizeSelectDefaultValue(payload || getNewVarInWorkflow('') as any))
|
||||||
const { type, label, variable, options, max_length } = tempPayload
|
const { type, label, variable, options, max_length } = tempPayload
|
||||||
const modalRef = useRef<HTMLDivElement>(null)
|
const modalRef = useRef<HTMLDivElement>(null)
|
||||||
const appDetail = useAppStore(state => state.appDetail)
|
const appDetail = useAppStore(state => state.appDetail)
|
||||||
@ -182,6 +188,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||||||
|
|
||||||
const newPayload = produce(tempPayload, (draft) => {
|
const newPayload = produce(tempPayload, (draft) => {
|
||||||
draft.type = type
|
draft.type = type
|
||||||
|
if (type === InputVarType.select)
|
||||||
|
draft.default = undefined
|
||||||
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
||||||
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
|
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
|
||||||
if (key !== 'max_length')
|
if (key !== 'max_length')
|
||||||
|
|||||||
@ -139,14 +139,14 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
content: item.answer,
|
content: item.answer,
|
||||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||||
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
|
feedback: item.feedbacks?.find(item => item.from_source === 'user'), // user feedback
|
||||||
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
|
adminFeedback: item.feedbacks?.find(item => item.from_source === 'admin'), // admin feedback
|
||||||
feedbackDisabled: false,
|
feedbackDisabled: false,
|
||||||
isAnswer: true,
|
isAnswer: true,
|
||||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||||
log: [
|
log: [
|
||||||
...item.message,
|
...(item.message ?? []),
|
||||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
...(item.message?.[item.message.length - 1]?.role !== 'assistant'
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@ -165,7 +165,7 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
|||||||
more: {
|
more: {
|
||||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||||
tokens: item.answer_tokens + item.message_tokens,
|
tokens: item.answer_tokens + item.message_tokens,
|
||||||
latency: item.provider_response_latency.toFixed(2),
|
latency: (item.provider_response_latency ?? 0).toFixed(2),
|
||||||
},
|
},
|
||||||
citation: item.metadata?.retriever_resources,
|
citation: item.metadata?.retriever_resources,
|
||||||
annotation: (() => {
|
annotation: (() => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine, RiDraftLine } from '@remixicon/react'
|
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine, RiDraftLine, RiRefreshLine } from '@remixicon/react'
|
||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -17,6 +17,7 @@ type IBatchActionProps = {
|
|||||||
onBatchDelete: () => Promise<void>
|
onBatchDelete: () => Promise<void>
|
||||||
onArchive?: () => void
|
onArchive?: () => void
|
||||||
onEditMetadata?: () => void
|
onEditMetadata?: () => void
|
||||||
|
onBatchReIndex?: () => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ const BatchAction: FC<IBatchActionProps> = ({
|
|||||||
onArchive,
|
onArchive,
|
||||||
onBatchDelete,
|
onBatchDelete,
|
||||||
onEditMetadata,
|
onEditMetadata,
|
||||||
|
onBatchReIndex,
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -91,6 +93,16 @@ const BatchAction: FC<IBatchActionProps> = ({
|
|||||||
<span className="px-0.5">{t(`${i18nPrefix}.archive`)}</span>
|
<span className="px-0.5">{t(`${i18nPrefix}.archive`)}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{onBatchReIndex && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="gap-x-0.5 px-3"
|
||||||
|
onClick={onBatchReIndex}
|
||||||
|
>
|
||||||
|
<RiRefreshLine className="size-4" />
|
||||||
|
<span className="px-0.5">{t(`${i18nPrefix}.reIndex`)}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
destructive
|
destructive
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '
|
|||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
import { ChunkingMode, DataSourceType, DocumentActionType } from '@/models/datasets'
|
import { ChunkingMode, DataSourceType, DocumentActionType } from '@/models/datasets'
|
||||||
import { DatasourceType } from '@/models/pipeline'
|
import { DatasourceType } from '@/models/pipeline'
|
||||||
import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable } from '@/service/knowledge/use-document'
|
import { useDocumentArchive, useDocumentBatchRetryIndex, useDocumentDelete, useDocumentDisable, useDocumentEnable } from '@/service/knowledge/use-document'
|
||||||
import { asyncRunSafe } from '@/utils'
|
import { asyncRunSafe } from '@/utils'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
@ -220,6 +220,7 @@ const DocumentList: FC<IDocumentListProps> = ({
|
|||||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||||
const { mutateAsync: disableDocument } = useDocumentDisable()
|
const { mutateAsync: disableDocument } = useDocumentDisable()
|
||||||
const { mutateAsync: deleteDocument } = useDocumentDelete()
|
const { mutateAsync: deleteDocument } = useDocumentDelete()
|
||||||
|
const { mutateAsync: retryIndexDocument } = useDocumentBatchRetryIndex()
|
||||||
|
|
||||||
const handleAction = (actionName: DocumentActionType) => {
|
const handleAction = (actionName: DocumentActionType) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
@ -250,6 +251,22 @@ const DocumentList: FC<IDocumentListProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBatchReIndex = async () => {
|
||||||
|
const [e] = await asyncRunSafe<CommonResponse>(retryIndexDocument({ datasetId, documentIds: selectedIds }))
|
||||||
|
if (!e) {
|
||||||
|
onSelectedIdChange([])
|
||||||
|
Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||||
|
onUpdate()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasErrorDocumentsSelected = useMemo(() => {
|
||||||
|
return localDocs.some(doc => selectedIds.includes(doc.id) && doc.display_status === 'error')
|
||||||
|
}, [localDocs, selectedIds])
|
||||||
|
|
||||||
const getFileExtension = useCallback((fileName: string): string => {
|
const getFileExtension = useCallback((fileName: string): string => {
|
||||||
if (!fileName)
|
if (!fileName)
|
||||||
return ''
|
return ''
|
||||||
@ -447,6 +464,7 @@ const DocumentList: FC<IDocumentListProps> = ({
|
|||||||
onBatchDisable={handleAction(DocumentActionType.disable)}
|
onBatchDisable={handleAction(DocumentActionType.disable)}
|
||||||
onBatchDelete={handleAction(DocumentActionType.delete)}
|
onBatchDelete={handleAction(DocumentActionType.delete)}
|
||||||
onEditMetadata={showEditModal}
|
onEditMetadata={showEditModal}
|
||||||
|
onBatchReIndex={hasErrorDocumentsSelected ? handleBatchReIndex : undefined}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
onSelectedIdChange([])
|
onSelectedIdChange([])
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import Records from './components/records'
|
|||||||
import ResultItem from './components/result-item'
|
import ResultItem from './components/result-item'
|
||||||
import ResultItemExternal from './components/result-item-external'
|
import ResultItemExternal from './components/result-item-external'
|
||||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||||
import s from './style.module.css'
|
|
||||||
|
|
||||||
const limit = 10
|
const limit = 10
|
||||||
|
|
||||||
@ -115,8 +114,8 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
}, [isMobile, setShowRightPanel])
|
}, [isMobile, setShowRightPanel])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.container}>
|
<div className="relative flex h-full w-full gap-x-6 overflow-y-auto pl-6">
|
||||||
<div className="flex flex-col px-6 py-3">
|
<div className="flex min-w-0 flex-1 flex-col py-3">
|
||||||
<div className="mb-4 flex flex-col justify-center">
|
<div className="mb-4 flex flex-col justify-center">
|
||||||
<h1 className="text-base font-semibold text-text-primary">{t('datasetHitTesting.title')}</h1>
|
<h1 className="text-base font-semibold text-text-primary">{t('datasetHitTesting.title')}</h1>
|
||||||
<p className="mt-0.5 text-[13px] font-normal leading-4 text-text-tertiary">{t('datasetHitTesting.desc')}</p>
|
<p className="mt-0.5 text-[13px] font-normal leading-4 text-text-tertiary">{t('datasetHitTesting.desc')}</p>
|
||||||
@ -161,7 +160,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
onClose={hideRightPanel}
|
onClose={hideRightPanel}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col pt-3">
|
<div className="flex min-w-0 flex-1 flex-col pt-3">
|
||||||
{isRetrievalLoading
|
{isRetrievalLoading
|
||||||
? (
|
? (
|
||||||
<div className="flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3">
|
<div className="flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3">
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
.container {
|
|
||||||
@apply flex h-full w-full relative overflow-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container>div {
|
|
||||||
@apply flex-1 h-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commonIcon {
|
|
||||||
@apply w-3.5 h-3.5 inline-block align-middle;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center center;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app_icon {
|
|
||||||
background-image: url(./assets/grid.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hit_testing_icon {
|
|
||||||
background-image: url(../documents/assets/target.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin_icon {
|
|
||||||
background-image: url(./assets/plugin.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardWrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(284px, auto));
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-auto-rows: 216px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clockWrapper {
|
|
||||||
border: 0.5px solid #eaecf5;
|
|
||||||
@apply rounded-lg w-11 h-11 flex justify-center items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clockIcon {
|
|
||||||
mask-image: url(./assets/clock.svg);
|
|
||||||
@apply bg-gray-500;
|
|
||||||
}
|
|
||||||
23
web/app/components/devtools.tsx
Normal file
23
web/app/components/devtools.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||||
|
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
|
||||||
|
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export function TanStackDevtoolsWrapper() {
|
||||||
|
return (
|
||||||
|
<TanStackDevtools
|
||||||
|
plugins={[
|
||||||
|
// Query Devtools (Official Plugin)
|
||||||
|
{
|
||||||
|
name: 'React Query',
|
||||||
|
render: () => <ReactQueryDevtoolsPanel />,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Form Devtools (Official Plugin)
|
||||||
|
formDevtoolsPlugin(),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,14 +3,14 @@
|
|||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
import { IS_DEV } from '@/config'
|
||||||
|
|
||||||
const SentryInitializer = ({
|
const SentryInitializer = ({
|
||||||
children,
|
children,
|
||||||
}: { children: React.ReactElement }) => {
|
}: { children: React.ReactElement }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
|
const SENTRY_DSN = document?.body?.getAttribute('data-public-sentry-dsn')
|
||||||
if (!isDevelopment && SENTRY_DSN) {
|
if (!IS_DEV && SENTRY_DSN) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_DSN,
|
dsn: SENTRY_DSN,
|
||||||
integrations: [
|
integrations: [
|
||||||
|
|||||||
@ -48,6 +48,7 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||||||
|
|
||||||
const [editFirst, setEditFirst] = useState(!isAdd)
|
const [editFirst, setEditFirst] = useState(!isAdd)
|
||||||
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
|
const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || [])
|
||||||
|
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
||||||
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
|
const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd
|
||||||
? {
|
? {
|
||||||
provider: '',
|
provider: '',
|
||||||
@ -67,6 +68,15 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||||||
|
|
||||||
const originalProvider = isEdit ? payload.provider : ''
|
const originalProvider = isEdit ? payload.provider : ''
|
||||||
|
|
||||||
|
// Sync customCollection state when payload changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEdit) {
|
||||||
|
setCustomCollection(payload)
|
||||||
|
setParamsSchemas(payload.tools || [])
|
||||||
|
setLabels(payload.labels || [])
|
||||||
|
}
|
||||||
|
}, [isEdit, payload])
|
||||||
|
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||||
const emoji = customCollection.icon
|
const emoji = customCollection.icon
|
||||||
const setEmoji = (emoji: Emoji) => {
|
const setEmoji = (emoji: Emoji) => {
|
||||||
@ -124,7 +134,6 @@ const EditCustomCollectionModal: FC<Props> = ({
|
|||||||
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
|
const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
|
||||||
const [isShowTestApi, setIsShowTestApi] = useState(false)
|
const [isShowTestApi, setIsShowTestApi] = useState(false)
|
||||||
|
|
||||||
const [labels, setLabels] = useState<string[]>(payload?.labels || [])
|
|
||||||
const handleLabelSelect = (value: string[]) => {
|
const handleLabelSelect = (value: string[]) => {
|
||||||
setLabels(value)
|
setLabels(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,9 +100,28 @@ const ProviderDetail = ({
|
|||||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||||
const [deleteAction, setDeleteAction] = useState('')
|
const [deleteAction, setDeleteAction] = useState('')
|
||||||
|
|
||||||
|
const getCustomProvider = useCallback(async () => {
|
||||||
|
setIsDetailLoading(true)
|
||||||
|
const res = await fetchCustomCollection(collection.name)
|
||||||
|
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
||||||
|
if (res.credentials.api_key_value)
|
||||||
|
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
||||||
|
}
|
||||||
|
setCustomCollection({
|
||||||
|
...res,
|
||||||
|
labels: collection.labels,
|
||||||
|
provider: collection.name,
|
||||||
|
})
|
||||||
|
setIsDetailLoading(false)
|
||||||
|
}, [collection.labels, collection.name])
|
||||||
|
|
||||||
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||||
await updateCustomCollection(data)
|
await updateCustomCollection(data)
|
||||||
onRefreshData()
|
onRefreshData()
|
||||||
|
await getCustomProvider()
|
||||||
|
// Use fresh data from form submission to avoid race condition with collection.labels
|
||||||
|
setCustomCollection(prev => prev ? { ...prev, labels: data.labels } : null)
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('common.api.actionSuccess'),
|
message: t('common.api.actionSuccess'),
|
||||||
@ -118,20 +137,6 @@ const ProviderDetail = ({
|
|||||||
})
|
})
|
||||||
setIsShowEditCustomCollectionModal(false)
|
setIsShowEditCustomCollectionModal(false)
|
||||||
}
|
}
|
||||||
const getCustomProvider = useCallback(async () => {
|
|
||||||
setIsDetailLoading(true)
|
|
||||||
const res = await fetchCustomCollection(collection.name)
|
|
||||||
if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
|
|
||||||
if (res.credentials.api_key_value)
|
|
||||||
res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
|
|
||||||
}
|
|
||||||
setCustomCollection({
|
|
||||||
...res,
|
|
||||||
labels: collection.labels,
|
|
||||||
provider: collection.name,
|
|
||||||
})
|
|
||||||
setIsDetailLoading(false)
|
|
||||||
}, [collection.labels, collection.name])
|
|
||||||
// workflow provider
|
// workflow provider
|
||||||
const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
|
const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
|
||||||
const getWorkflowToolProvider = useCallback(async () => {
|
const getWorkflowToolProvider = useCallback(async () => {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 rounded-lg border border-transparent',
|
'rounded-lg border border-transparent p-2',
|
||||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||||
)}
|
)}
|
||||||
onClick={handleViewVersionHistory}
|
onClick={handleViewVersionHistory}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import ReactFlow, {
|
|||||||
useReactFlow,
|
useReactFlow,
|
||||||
useStoreApi,
|
useStoreApi,
|
||||||
} from 'reactflow'
|
} from 'reactflow'
|
||||||
|
import { IS_DEV } from '@/config'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import {
|
import {
|
||||||
useAllBuiltInTools,
|
useAllBuiltInTools,
|
||||||
@ -361,7 +362,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||||||
}
|
}
|
||||||
}, [schemaTypeDefinitions, fetchInspectVars, isLoadedVars, vars, customTools, buildInTools, workflowTools, mcpTools, dataSourceList])
|
}, [schemaTypeDefinitions, fetchInspectVars, isLoadedVars, vars, customTools, buildInTools, workflowTools, mcpTools, dataSourceList])
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (IS_DEV) {
|
||||||
store.getState().onError = (code, message) => {
|
store.getState().onError = (code, message) => {
|
||||||
if (code === '002')
|
if (code === '002')
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import BaseForm from '../components/base/form/form-scenarios/base'
|
|
||||||
import { BaseFieldType } from '../components/base/form/form-scenarios/base/types'
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-full items-center justify-center p-20">
|
|
||||||
<div className="w-[400px] rounded-lg border border-components-panel-border bg-components-panel-bg">
|
|
||||||
<BaseForm
|
|
||||||
initialData={{
|
|
||||||
type: 'option_1',
|
|
||||||
variable: 'test',
|
|
||||||
label: 'Test',
|
|
||||||
maxLength: 48,
|
|
||||||
required: true,
|
|
||||||
}}
|
|
||||||
configurations={[
|
|
||||||
{
|
|
||||||
type: BaseFieldType.textInput,
|
|
||||||
variable: 'variable',
|
|
||||||
label: 'Variable',
|
|
||||||
required: true,
|
|
||||||
showConditions: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: BaseFieldType.textInput,
|
|
||||||
variable: 'label',
|
|
||||||
label: 'Label',
|
|
||||||
required: true,
|
|
||||||
showConditions: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: BaseFieldType.numberInput,
|
|
||||||
variable: 'maxLength',
|
|
||||||
label: 'Max Length',
|
|
||||||
required: true,
|
|
||||||
showConditions: [],
|
|
||||||
max: 100,
|
|
||||||
min: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: BaseFieldType.checkbox,
|
|
||||||
variable: 'required',
|
|
||||||
label: 'Required',
|
|
||||||
required: true,
|
|
||||||
showConditions: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: BaseFieldType.select,
|
|
||||||
variable: 'type',
|
|
||||||
label: 'Type',
|
|
||||||
required: true,
|
|
||||||
showConditions: [],
|
|
||||||
options: [
|
|
||||||
{ label: 'Option 1', value: 'option_1' },
|
|
||||||
{ label: 'Option 2', value: 'option_2' },
|
|
||||||
{ label: 'Option 3', value: 'option_3' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onSubmit={(value) => {
|
|
||||||
console.log('onSubmit', value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
import type { FC, PropsWithChildren } from 'react'
|
import type { FC, PropsWithChildren } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
import { lazy, Suspense } from 'react'
|
||||||
|
import { IS_DEV } from '@/config'
|
||||||
|
|
||||||
|
const TanStackDevtoolsWrapper = lazy(() =>
|
||||||
|
import('@/app/components/devtools').then(module => ({
|
||||||
|
default: module.TanStackDevtoolsWrapper,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
const STALE_TIME = 1000 * 60 * 30 // 30 minutes
|
const STALE_TIME = 1000 * 60 * 30 // 30 minutes
|
||||||
|
|
||||||
@ -19,7 +26,11 @@ export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
{children}
|
{children}
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
{IS_DEV && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<TanStackDevtoolsWrapper />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,6 +170,7 @@ const translation = {
|
|||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
archive: 'Archive',
|
archive: 'Archive',
|
||||||
|
reIndex: 'Re-index',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -170,6 +170,7 @@ const translation = {
|
|||||||
enable: '启用',
|
enable: '启用',
|
||||||
disable: '禁用',
|
disable: '禁用',
|
||||||
archive: '归档',
|
archive: '归档',
|
||||||
|
reIndex: '重新索引',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.11.2",
|
"version": "1.11.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa",
|
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=v22.11.0"
|
"node": ">=v22.11.0"
|
||||||
},
|
},
|
||||||
@ -71,7 +71,6 @@
|
|||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-form": "^1.23.7",
|
"@tanstack/react-form": "^1.23.7",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
|
||||||
"abcjs": "^6.5.2",
|
"abcjs": "^6.5.2",
|
||||||
"ahooks": "^3.9.5",
|
"ahooks": "^3.9.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@ -134,7 +133,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"scheduler": "^0.26.0",
|
"scheduler": "^0.27.0",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
@ -163,6 +162,9 @@
|
|||||||
"@storybook/addon-themes": "9.1.13",
|
"@storybook/addon-themes": "9.1.13",
|
||||||
"@storybook/nextjs": "9.1.13",
|
"@storybook/nextjs": "9.1.13",
|
||||||
"@storybook/react": "9.1.13",
|
"@storybook/react": "9.1.13",
|
||||||
|
"@tanstack/react-devtools": "^0.9.0",
|
||||||
|
"@tanstack/react-form-devtools": "^0.2.9",
|
||||||
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
|||||||
306
web/pnpm-lock.yaml
generated
306
web/pnpm-lock.yaml
generated
@ -129,9 +129,6 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.90.5
|
specifier: ^5.90.5
|
||||||
version: 5.90.12(react@19.2.3)
|
version: 5.90.12(react@19.2.3)
|
||||||
'@tanstack/react-query-devtools':
|
|
||||||
specifier: ^5.90.2
|
|
||||||
version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)
|
|
||||||
abcjs:
|
abcjs:
|
||||||
specifier: ^6.5.2
|
specifier: ^6.5.2
|
||||||
version: 6.5.2
|
version: 6.5.2
|
||||||
@ -319,8 +316,8 @@ importers:
|
|||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
scheduler:
|
scheduler:
|
||||||
specifier: ^0.26.0
|
specifier: ^0.27.0
|
||||||
version: 0.26.0
|
version: 0.27.0
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.7.3
|
specifier: ^7.7.3
|
||||||
version: 7.7.3
|
version: 7.7.3
|
||||||
@ -338,7 +335,7 @@ importers:
|
|||||||
version: 7.0.19
|
version: 7.0.19
|
||||||
use-context-selector:
|
use-context-selector:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0(react@19.2.3)(scheduler@0.26.0)
|
version: 2.0.0(react@19.2.3)(scheduler@0.27.0)
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
@ -400,6 +397,15 @@ importers:
|
|||||||
'@storybook/react':
|
'@storybook/react':
|
||||||
specifier: 9.1.13
|
specifier: 9.1.13
|
||||||
version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)
|
version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)
|
||||||
|
'@tanstack/react-devtools':
|
||||||
|
specifier: ^0.9.0
|
||||||
|
version: 0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)
|
||||||
|
'@tanstack/react-form-devtools':
|
||||||
|
specifier: ^0.2.9
|
||||||
|
version: 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
|
||||||
|
'@tanstack/react-query-devtools':
|
||||||
|
specifier: ^5.90.2
|
||||||
|
version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)
|
||||||
'@testing-library/dom':
|
'@testing-library/dom':
|
||||||
specifier: ^10.4.1
|
specifier: ^10.4.1
|
||||||
version: 10.4.1
|
version: 10.4.1
|
||||||
@ -3166,6 +3172,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@solid-primitives/event-listener@2.4.3':
|
||||||
|
resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
|
'@solid-primitives/keyboard@1.3.3':
|
||||||
|
resolution: {integrity: sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
|
'@solid-primitives/resize-observer@2.1.3':
|
||||||
|
resolution: {integrity: sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
|
'@solid-primitives/rootless@1.5.2':
|
||||||
|
resolution: {integrity: sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
|
'@solid-primitives/static-store@0.1.2':
|
||||||
|
resolution: {integrity: sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
|
'@solid-primitives/utils@6.3.2':
|
||||||
|
resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: ^1.6.12
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
@ -3305,13 +3341,67 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
|
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
|
||||||
|
|
||||||
|
'@tanstack/devtools-client@0.0.5':
|
||||||
|
resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tanstack/devtools-event-bus@0.4.0':
|
||||||
|
resolution: {integrity: sha512-1t+/csFuDzi+miDxAOh6Xv7VDE80gJEItkTcAZLjV5MRulbO/W8ocjHLI2Do/p2r2/FBU0eKCRTpdqvXaYoHpQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@tanstack/devtools-event-client@0.3.5':
|
'@tanstack/devtools-event-client@0.3.5':
|
||||||
resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
|
resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tanstack/devtools-event-client@0.4.0':
|
||||||
|
resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tanstack/devtools-ui@0.4.4':
|
||||||
|
resolution: {integrity: sha512-5xHXFyX3nom0UaNfiOM92o6ziaHjGo3mcSGe2HD5Xs8dWRZNpdZ0Smd0B9ddEhy0oB+gXyMzZgUJb9DmrZV0Mg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: '>=1.9.7'
|
||||||
|
|
||||||
|
'@tanstack/devtools-utils@0.0.9':
|
||||||
|
resolution: {integrity: sha512-tCObM6wbEjuHeGNs3JDhrqBhoMxpJpVuVIg5Kc33EmUI1ZO7KLpC1277Qf6AmSWy3aVOreGwn3y5bJzxmAJNXg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ~19.2.7
|
||||||
|
react: '>=17.0.0'
|
||||||
|
solid-js: '>=1.9.7'
|
||||||
|
vue: '>=3.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
solid-js:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tanstack/devtools@0.10.1':
|
||||||
|
resolution: {integrity: sha512-1gtPmCDXV4Pl1nVtoqwjV0tc4E9GMuFtlkBX1Lz1KfqI3W9JojT5YsVifOQ/g8BTQ5w5+tyIANwHU7WYgLq/MQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: '>=1.9.7'
|
||||||
|
|
||||||
'@tanstack/form-core@1.27.1':
|
'@tanstack/form-core@1.27.1':
|
||||||
resolution: {integrity: sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==}
|
resolution: {integrity: sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==}
|
||||||
|
|
||||||
|
'@tanstack/form-core@1.27.6':
|
||||||
|
resolution: {integrity: sha512-1C4PUpOcCpivddKxtAeqdeqncxnPKiPpTVDRknDExCba+6zCsAjxgL+p3qYA3hu+EFyUAdW71rU+uqYbEa7qqA==}
|
||||||
|
|
||||||
|
'@tanstack/form-devtools@0.2.9':
|
||||||
|
resolution: {integrity: sha512-KOJiwvlFPsHeuWXvHUXRVdciXG1OPhg1c476MsLre0YLdaw1jeMlDYSlqq7sdEULX+2Sg/lhNpX86QbQuxzd2A==}
|
||||||
|
peerDependencies:
|
||||||
|
solid-js: '>=1.9.9'
|
||||||
|
|
||||||
|
'@tanstack/pacer-lite@0.1.1':
|
||||||
|
resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@tanstack/pacer@0.15.4':
|
'@tanstack/pacer@0.15.4':
|
||||||
resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
|
resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -3322,6 +3412,20 @@ packages:
|
|||||||
'@tanstack/query-devtools@5.91.1':
|
'@tanstack/query-devtools@5.91.1':
|
||||||
resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==}
|
resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==}
|
||||||
|
|
||||||
|
'@tanstack/react-devtools@0.9.0':
|
||||||
|
resolution: {integrity: sha512-Lq0svXOTG5N61SHgx8F0on6zz2GB0kmFjN/yyfNLrJyRgJ+U3jYFRd9ti3uBPABsXzHQMHYYujnTXrOYp/OaUg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ~19.2.7
|
||||||
|
'@types/react-dom': ~19.2.3
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
|
'@tanstack/react-form-devtools@0.2.9':
|
||||||
|
resolution: {integrity: sha512-wg0xrcVY8evIFGVHrnl9s+/9ENzuVbqv5Ru4HyAJjjL4uECtl6KdDJsi0lZdOyoM1UYEQoVdcN8jfBbxkA3q1g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
'@tanstack/react-form@1.27.1':
|
'@tanstack/react-form@1.27.1':
|
||||||
resolution: {integrity: sha512-HKP0Ew2ae9AL5vU1PkJ+oAC2p+xBtA905u0fiNLzlfn1vLkBxenfg5L6TOA+rZITHpQsSo10tqwc5Yw6qn8Mpg==}
|
resolution: {integrity: sha512-HKP0Ew2ae9AL5vU1PkJ+oAC2p+xBtA905u0fiNLzlfn1vLkBxenfg5L6TOA+rZITHpQsSo10tqwc5Yw6qn8Mpg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3591,6 +3695,9 @@ packages:
|
|||||||
'@types/node@20.19.26':
|
'@types/node@20.19.26':
|
||||||
resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==}
|
resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==}
|
||||||
|
|
||||||
|
'@types/node@20.19.27':
|
||||||
|
resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==}
|
||||||
|
|
||||||
'@types/papaparse@5.5.1':
|
'@types/papaparse@5.5.1':
|
||||||
resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==}
|
resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==}
|
||||||
|
|
||||||
@ -5626,6 +5733,11 @@ packages:
|
|||||||
globrex@0.1.2:
|
globrex@0.1.2:
|
||||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
|
|
||||||
|
goober@2.1.18:
|
||||||
|
resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
|
||||||
|
peerDependencies:
|
||||||
|
csstype: ^3.0.10
|
||||||
|
|
||||||
got@11.8.6:
|
got@11.8.6:
|
||||||
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
|
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
|
||||||
engines: {node: '>=10.19.0'}
|
engines: {node: '>=10.19.0'}
|
||||||
@ -6200,8 +6312,8 @@ packages:
|
|||||||
lexical@0.38.2:
|
lexical@0.38.2:
|
||||||
resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==}
|
resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==}
|
||||||
|
|
||||||
lib0@0.2.115:
|
lib0@0.2.116:
|
||||||
resolution: {integrity: sha512-noaW4yNp6hCjOgDnWWxW0vGXE3kZQI5Kqiwz+jIWXavI9J9WyfJ9zjsbQlQlgjIbHBrvlA/x3TSIXBUJj+0L6g==}
|
resolution: {integrity: sha512-4zsosjzmt33rx5XjmFVYUAeLNh+BTeDTiwGdLt4muxiir2btsc60Nal0EvkvDRizg+pnlK1q+BtYi7M+d4eStw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -7638,9 +7750,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
engines: {node: '>=v12.22.7'}
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.26.0:
|
|
||||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@ -7684,6 +7793,16 @@ packages:
|
|||||||
serialize-javascript@6.0.2:
|
serialize-javascript@6.0.2:
|
||||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||||
|
|
||||||
|
seroval-plugins@1.3.3:
|
||||||
|
resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
seroval: ^1.0
|
||||||
|
|
||||||
|
seroval@1.3.2:
|
||||||
|
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
setimmediate@1.0.5:
|
setimmediate@1.0.5:
|
||||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
@ -7750,6 +7869,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
|
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
solid-js@1.9.10:
|
||||||
|
resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==}
|
||||||
|
|
||||||
sortablejs@1.15.6:
|
sortablejs@1.15.6:
|
||||||
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
|
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
|
||||||
|
|
||||||
@ -8581,6 +8703,7 @@ packages:
|
|||||||
whatwg-encoding@3.1.1:
|
whatwg-encoding@3.1.1:
|
||||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||||
|
|
||||||
whatwg-mimetype@3.0.0:
|
whatwg-mimetype@3.0.0:
|
||||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||||
@ -11531,6 +11654,40 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/is@4.6.0': {}
|
'@sindresorhus/is@4.6.0': {}
|
||||||
|
|
||||||
|
'@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
|
'@solid-primitives/keyboard@1.3.3(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/rootless': 1.5.2(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
|
'@solid-primitives/resize-observer@2.1.3(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/rootless': 1.5.2(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/static-store': 0.1.2(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
|
'@solid-primitives/rootless@1.5.2(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
|
'@solid-primitives/static-store@0.1.2(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/utils': 6.3.2(solid-js@1.9.10)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
|
'@solid-primitives/utils@6.3.2(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
solid-js: 1.9.10
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0': {}
|
'@standard-schema/utils@0.3.0': {}
|
||||||
@ -11758,14 +11915,84 @@ snapshots:
|
|||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2)
|
tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
|
'@tanstack/devtools-client@0.0.5':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools-event-client': 0.4.0
|
||||||
|
|
||||||
|
'@tanstack/devtools-event-bus@0.4.0':
|
||||||
|
dependencies:
|
||||||
|
ws: 8.18.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@tanstack/devtools-event-client@0.3.5': {}
|
'@tanstack/devtools-event-client@0.3.5': {}
|
||||||
|
|
||||||
|
'@tanstack/devtools-event-client@0.4.0': {}
|
||||||
|
|
||||||
|
'@tanstack/devtools-ui@0.4.4(csstype@3.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
clsx: 2.1.1
|
||||||
|
goober: 2.1.18(csstype@3.2.3)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- csstype
|
||||||
|
|
||||||
|
'@tanstack/devtools-utils@0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
react: 19.2.3
|
||||||
|
solid-js: 1.9.10
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- csstype
|
||||||
|
|
||||||
|
'@tanstack/devtools@0.10.1(csstype@3.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/keyboard': 1.3.3(solid-js@1.9.10)
|
||||||
|
'@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.10)
|
||||||
|
'@tanstack/devtools-client': 0.0.5
|
||||||
|
'@tanstack/devtools-event-bus': 0.4.0
|
||||||
|
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
|
||||||
|
clsx: 2.1.1
|
||||||
|
goober: 2.1.18(csstype@3.2.3)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- csstype
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@tanstack/form-core@1.27.1':
|
'@tanstack/form-core@1.27.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/devtools-event-client': 0.3.5
|
'@tanstack/devtools-event-client': 0.3.5
|
||||||
'@tanstack/pacer': 0.15.4
|
'@tanstack/pacer': 0.15.4
|
||||||
'@tanstack/store': 0.7.7
|
'@tanstack/store': 0.7.7
|
||||||
|
|
||||||
|
'@tanstack/form-core@1.27.6':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools-event-client': 0.4.0
|
||||||
|
'@tanstack/pacer-lite': 0.1.1
|
||||||
|
'@tanstack/store': 0.7.7
|
||||||
|
|
||||||
|
'@tanstack/form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
|
||||||
|
'@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
|
||||||
|
'@tanstack/form-core': 1.27.6
|
||||||
|
clsx: 2.1.1
|
||||||
|
dayjs: 1.11.19
|
||||||
|
goober: 2.1.18(csstype@3.2.3)
|
||||||
|
solid-js: 1.9.10
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- csstype
|
||||||
|
- react
|
||||||
|
- vue
|
||||||
|
|
||||||
|
'@tanstack/pacer-lite@0.1.1': {}
|
||||||
|
|
||||||
'@tanstack/pacer@0.15.4':
|
'@tanstack/pacer@0.15.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/devtools-event-client': 0.3.5
|
'@tanstack/devtools-event-client': 0.3.5
|
||||||
@ -11775,6 +12002,30 @@ snapshots:
|
|||||||
|
|
||||||
'@tanstack/query-devtools@5.91.1': {}
|
'@tanstack/query-devtools@5.91.1': {}
|
||||||
|
|
||||||
|
'@tanstack/react-devtools@0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools': 0.10.1(csstype@3.2.3)(solid-js@1.9.10)
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- csstype
|
||||||
|
- solid-js
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@tanstack/react-form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
|
||||||
|
'@tanstack/form-devtools': 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)
|
||||||
|
react: 19.2.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- csstype
|
||||||
|
- solid-js
|
||||||
|
- vue
|
||||||
|
|
||||||
'@tanstack/react-form@1.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
'@tanstack/react-form@1.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/form-core': 1.27.1
|
'@tanstack/form-core': 1.27.1
|
||||||
@ -12083,6 +12334,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@20.19.27':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/papaparse@5.5.1':
|
'@types/papaparse@5.5.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.15.0
|
'@types/node': 18.15.0
|
||||||
@ -14478,6 +14734,10 @@ snapshots:
|
|||||||
|
|
||||||
globrex@0.1.2: {}
|
globrex@0.1.2: {}
|
||||||
|
|
||||||
|
goober@2.1.18(csstype@3.2.3):
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
got@11.8.6:
|
got@11.8.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sindresorhus/is': 4.6.0
|
'@sindresorhus/is': 4.6.0
|
||||||
@ -14504,7 +14764,7 @@ snapshots:
|
|||||||
|
|
||||||
happy-dom@20.0.11:
|
happy-dom@20.0.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.26
|
'@types/node': 20.19.27
|
||||||
'@types/whatwg-mimetype': 3.0.2
|
'@types/whatwg-mimetype': 3.0.2
|
||||||
whatwg-mimetype: 3.0.0
|
whatwg-mimetype: 3.0.0
|
||||||
optional: true
|
optional: true
|
||||||
@ -15119,7 +15379,7 @@ snapshots:
|
|||||||
|
|
||||||
lexical@0.38.2: {}
|
lexical@0.38.2: {}
|
||||||
|
|
||||||
lib0@0.2.115:
|
lib0@0.2.116:
|
||||||
dependencies:
|
dependencies:
|
||||||
isomorphic.js: 0.2.5
|
isomorphic.js: 0.2.5
|
||||||
|
|
||||||
@ -17036,8 +17296,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.26.0: {}
|
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-utils@2.7.1:
|
schema-utils@2.7.1:
|
||||||
@ -17081,6 +17339,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes: 2.1.0
|
randombytes: 2.1.0
|
||||||
|
|
||||||
|
seroval-plugins@1.3.3(seroval@1.3.2):
|
||||||
|
dependencies:
|
||||||
|
seroval: 1.3.2
|
||||||
|
|
||||||
|
seroval@1.3.2: {}
|
||||||
|
|
||||||
setimmediate@1.0.5: {}
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
sha.js@2.4.12:
|
sha.js@2.4.12:
|
||||||
@ -17195,6 +17459,12 @@ snapshots:
|
|||||||
|
|
||||||
smol-toml@1.5.2: {}
|
smol-toml@1.5.2: {}
|
||||||
|
|
||||||
|
solid-js@1.9.10:
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.2.3
|
||||||
|
seroval: 1.3.2
|
||||||
|
seroval-plugins: 1.3.3(seroval@1.3.2)
|
||||||
|
|
||||||
sortablejs@1.15.6: {}
|
sortablejs@1.15.6: {}
|
||||||
|
|
||||||
source-list-map@2.0.1: {}
|
source-list-map@2.0.1: {}
|
||||||
@ -17720,10 +17990,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
use-context-selector@2.0.0(react@19.2.3)(scheduler@0.26.0):
|
use-context-selector@2.0.0(react@19.2.3)(scheduler@0.27.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
scheduler: 0.26.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
use-isomorphic-layout-effect@1.2.1(@types/react@19.2.7)(react@19.2.3):
|
use-isomorphic-layout-effect@1.2.1(@types/react@19.2.7)(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -18188,7 +18458,7 @@ snapshots:
|
|||||||
|
|
||||||
yjs@13.6.27:
|
yjs@13.6.27:
|
||||||
dependencies:
|
dependencies:
|
||||||
lib0: 0.2.115
|
lib0: 0.2.116
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
import { normalizeStatusForQuery } from '@/app/components/datasets/documents/status-filter'
|
import { normalizeStatusForQuery } from '@/app/components/datasets/documents/status-filter'
|
||||||
import { DocumentActionType } from '@/models/datasets'
|
import { DocumentActionType } from '@/models/datasets'
|
||||||
import { del, get, patch } from '../base'
|
import { del, get, patch, post } from '../base'
|
||||||
import { pauseDocIndexing, resumeDocIndexing } from '../datasets'
|
import { pauseDocIndexing, resumeDocIndexing } from '../datasets'
|
||||||
import { useInvalid } from '../use-base'
|
import { useInvalid } from '../use-base'
|
||||||
|
|
||||||
@ -163,3 +163,15 @@ export const useDocumentResume = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useDocumentBatchRetryIndex = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ datasetId, documentIds }: { datasetId: string, documentIds: string[] }) => {
|
||||||
|
return post<CommonResponse>(`/datasets/${datasetId}/retry`, {
|
||||||
|
body: {
|
||||||
|
document_ids: documentIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user