mirror of
https://github.com/langgenius/dify.git
synced 2026-07-01 03:16:51 +08:00
Compare commits
6 Commits
codex/agen
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 089c3f4af0 | |||
| 4b17f968a6 | |||
| baf2191ee3 | |||
| 9393df9d00 | |||
| b1bb6ef977 | |||
| 200f8b800f |
@ -16,10 +16,11 @@ import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from mimetypes import guess_type
|
||||
from typing import ClassVar
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||
from redis import RedisError
|
||||
from redis.exceptions import LockError
|
||||
from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
from yarl import URL
|
||||
@ -82,6 +83,10 @@ class PluginService:
|
||||
REDIS_TTL = 60 * 5 # 5 minutes
|
||||
PLUGIN_MODEL_PROVIDERS_REDIS_KEY_PREFIX = "plugin_model_providers:tenant_id:"
|
||||
PLUGIN_MODEL_PROVIDERS_GENERATION_REDIS_KEY_PREFIX = "plugin_model_providers_generation:tenant_id:"
|
||||
PLUGIN_MODEL_PROVIDERS_LOCK_REDIS_KEY_PREFIX = "plugin_model_providers_refresh_lock:tenant_id:"
|
||||
PLUGIN_MODEL_PROVIDERS_LOCK_TTL = 30
|
||||
PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_TIMEOUT = 2.0
|
||||
PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_INTERVAL = 0.05
|
||||
PLUGIN_INSTALL_TASK_TERMINAL_STATUSES = (PluginInstallTaskStatus.Success, PluginInstallTaskStatus.Failed)
|
||||
# Mirror the detail-panel endpoint query size so list reconciliation and
|
||||
# the visible endpoint drawer exercise the same daemon pagination path.
|
||||
@ -98,6 +103,10 @@ class PluginService:
|
||||
def _get_plugin_model_providers_generation_cache_key(cls, tenant_id: str) -> str:
|
||||
return f"{cls.PLUGIN_MODEL_PROVIDERS_GENERATION_REDIS_KEY_PREFIX}{tenant_id}"
|
||||
|
||||
@classmethod
|
||||
def _get_plugin_model_providers_lock_key(cls, tenant_id: str, generation: int) -> str:
|
||||
return f"{cls.PLUGIN_MODEL_PROVIDERS_LOCK_REDIS_KEY_PREFIX}{tenant_id}:generation:{generation}"
|
||||
|
||||
@staticmethod
|
||||
def _get_provider_short_name_alias(provider: PluginModelProviderEntity) -> str:
|
||||
"""
|
||||
@ -197,32 +206,41 @@ class PluginService:
|
||||
cls, tenant_id: str, *, client: PluginModelClient | None = None
|
||||
) -> tuple[ProviderEntity, ...] | None:
|
||||
generation = cls._load_plugin_model_providers_generation(tenant_id)
|
||||
cached_providers, _ = cls._load_cached_plugin_model_providers_for_generation(tenant_id, generation)
|
||||
return cached_providers
|
||||
|
||||
@classmethod
|
||||
def _load_cached_plugin_model_providers_for_generation(
|
||||
cls, tenant_id: str, generation: int | None
|
||||
) -> tuple[tuple[ProviderEntity, ...] | None, bool]:
|
||||
if generation is not None:
|
||||
in_memory_cached_providers = cls._load_in_memory_plugin_model_providers(tenant_id, generation)
|
||||
if in_memory_cached_providers is not None:
|
||||
return in_memory_cached_providers
|
||||
return in_memory_cached_providers, True
|
||||
|
||||
if generation is None:
|
||||
return None, False
|
||||
|
||||
cache_keys = []
|
||||
if generation is not None:
|
||||
cache_keys.append(cls._get_plugin_model_providers_cache_key(tenant_id, generation))
|
||||
if generation == 0:
|
||||
cache_keys.append(cls._get_plugin_model_providers_cache_key(tenant_id))
|
||||
cache_keys.append(cls._get_plugin_model_providers_cache_key(tenant_id, generation))
|
||||
if generation == 0:
|
||||
cache_keys.append(cls._get_plugin_model_providers_cache_key(tenant_id))
|
||||
|
||||
if not cache_keys:
|
||||
return None
|
||||
return None, True
|
||||
|
||||
try:
|
||||
cached_provider_entries = redis_client.mget(cache_keys)
|
||||
except (RedisError, RuntimeError):
|
||||
except (LockError, RedisError, RuntimeError):
|
||||
logger.warning("Failed to read cached plugin model providers for tenant %s.", tenant_id, exc_info=True)
|
||||
return None
|
||||
return None, False
|
||||
|
||||
if len(cached_provider_entries) != len(cache_keys):
|
||||
logger.warning(
|
||||
"Unexpected cached plugin model providers response size for tenant %s.",
|
||||
tenant_id,
|
||||
)
|
||||
return None
|
||||
return None, False
|
||||
|
||||
for cache_key, cached_providers in zip(cache_keys, cached_provider_entries):
|
||||
if not cached_providers:
|
||||
@ -232,7 +250,7 @@ class PluginService:
|
||||
providers = tuple(_provider_entities_adapter.validate_json(cached_providers))
|
||||
if generation is not None:
|
||||
cls._store_in_memory_plugin_model_providers(tenant_id, generation, providers)
|
||||
return providers
|
||||
return providers, True
|
||||
except (TypeError, ValueError, ValidationError):
|
||||
logger.warning(
|
||||
"Invalid cached plugin model providers for tenant %s; deleting cache key %s.",
|
||||
@ -249,7 +267,7 @@ class PluginService:
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return None
|
||||
return None, True
|
||||
|
||||
@classmethod
|
||||
def _store_cached_plugin_model_providers(
|
||||
@ -262,6 +280,49 @@ class PluginService:
|
||||
except (RedisError, RuntimeError):
|
||||
logger.warning("Failed to cache plugin model providers for tenant %s.", tenant_id, exc_info=True)
|
||||
|
||||
@classmethod
|
||||
def _try_acquire_plugin_model_providers_lock(cls, tenant_id: str, generation: int) -> tuple[Any | None, bool]:
|
||||
lock_key = cls._get_plugin_model_providers_lock_key(tenant_id, generation)
|
||||
try:
|
||||
lock = redis_client.lock(lock_key, timeout=cls.PLUGIN_MODEL_PROVIDERS_LOCK_TTL, blocking=False)
|
||||
acquired = lock.acquire(blocking=False)
|
||||
except (RedisError, RuntimeError):
|
||||
logger.warning(
|
||||
"Failed to acquire plugin model providers refresh lock for tenant %s.",
|
||||
tenant_id,
|
||||
exc_info=True,
|
||||
)
|
||||
return None, False
|
||||
|
||||
if not acquired:
|
||||
return None, True
|
||||
|
||||
return lock, True
|
||||
|
||||
@classmethod
|
||||
def _release_plugin_model_providers_lock(cls, tenant_id: str, lock: Any) -> None:
|
||||
try:
|
||||
lock.release()
|
||||
except (LockError, RedisError, RuntimeError):
|
||||
logger.warning(
|
||||
"Failed to release plugin model providers refresh lock for tenant %s.",
|
||||
tenant_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _wait_for_plugin_model_providers_refresh(
|
||||
cls, tenant_id: str, *, client: PluginModelClient | None = None
|
||||
) -> tuple[ProviderEntity, ...] | None:
|
||||
deadline = time.monotonic() + cls.PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(cls.PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_INTERVAL)
|
||||
cached_providers = cls._load_cached_plugin_model_providers(tenant_id, client=client)
|
||||
if cached_providers is not None:
|
||||
return cached_providers
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def invalidate_plugin_model_providers_cache(cls, tenant_id: str) -> None:
|
||||
"""Invalidate tenant-scoped provider metadata across Redis and worker-local mirrors."""
|
||||
@ -287,21 +348,38 @@ class PluginService:
|
||||
are intentionally owned by this service so tenant isolation and cache
|
||||
expiry are handled in one place.
|
||||
"""
|
||||
cached_providers = cls._load_cached_plugin_model_providers(tenant_id, client=client)
|
||||
generation = cls._load_plugin_model_providers_generation(tenant_id)
|
||||
cached_providers, cache_available = cls._load_cached_plugin_model_providers_for_generation(
|
||||
tenant_id, generation
|
||||
)
|
||||
if cached_providers is not None:
|
||||
return cached_providers
|
||||
|
||||
refresh_lock: Any | None = None
|
||||
refresh_generation = generation
|
||||
if generation is not None and cache_available:
|
||||
lock_wait_deadline = time.monotonic() + cls.PLUGIN_MODEL_PROVIDERS_LOCK_TTL
|
||||
while time.monotonic() < lock_wait_deadline:
|
||||
refresh_lock, lock_available = cls._try_acquire_plugin_model_providers_lock(tenant_id, generation)
|
||||
if refresh_lock is not None or not lock_available:
|
||||
break
|
||||
refreshed_providers = cls._wait_for_plugin_model_providers_refresh(tenant_id, client=client)
|
||||
if refreshed_providers is not None:
|
||||
return refreshed_providers
|
||||
|
||||
model_client = client or PluginModelClient()
|
||||
providers = tuple(
|
||||
cls._to_provider_entity(provider) for provider in model_client.fetch_model_providers(tenant_id)
|
||||
)
|
||||
if not providers:
|
||||
try:
|
||||
providers = tuple(
|
||||
cls._to_provider_entity(provider) for provider in model_client.fetch_model_providers(tenant_id)
|
||||
)
|
||||
generation = cls._load_plugin_model_providers_generation(tenant_id)
|
||||
if generation is not None and generation == refresh_generation:
|
||||
cls._store_in_memory_plugin_model_providers(tenant_id, generation, providers)
|
||||
cls._store_cached_plugin_model_providers(tenant_id, generation, providers)
|
||||
return providers
|
||||
generation = cls._load_plugin_model_providers_generation(tenant_id)
|
||||
if generation is not None:
|
||||
cls._store_in_memory_plugin_model_providers(tenant_id, generation, providers)
|
||||
cls._store_cached_plugin_model_providers(tenant_id, generation, providers)
|
||||
return providers
|
||||
finally:
|
||||
if refresh_lock is not None:
|
||||
cls._release_plugin_model_providers_lock(tenant_id, refresh_lock)
|
||||
|
||||
@staticmethod
|
||||
def fetch_latest_plugin_version(plugin_ids: Sequence[str]) -> Mapping[str, LatestPluginCache | None]:
|
||||
|
||||
@ -701,16 +701,29 @@ def _delete_records(query_sql: str, params: dict[str, Any], delete_func: Callabl
|
||||
if not rows:
|
||||
break
|
||||
|
||||
success_count = 0
|
||||
for i in rows:
|
||||
record_id = str(i.id)
|
||||
try:
|
||||
delete_func(session, record_id)
|
||||
logger.info(click.style(f"Deleted {name} {record_id}", fg="green"))
|
||||
session.commit()
|
||||
success_count += 1
|
||||
except Exception:
|
||||
logger.exception("Error occurred while deleting %s %s", name, record_id)
|
||||
# continue with next record even if one deletion fails
|
||||
session.rollback()
|
||||
break
|
||||
session.commit()
|
||||
continue
|
||||
|
||||
rs.close()
|
||||
|
||||
# If we couldn't delete ANY records in this batch, we must break out of the while loop
|
||||
# to prevent an infinite loop where we keep fetching the same failing records.
|
||||
if success_count == 0:
|
||||
logger.warning(
|
||||
click.style(
|
||||
f"Failed to delete any {name} in the current batch. Stopping to prevent infinite loop.",
|
||||
fg="yellow",
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
@ -44,6 +44,29 @@ class _FakeRedis:
|
||||
def delete(self, key: str) -> None:
|
||||
self._values.pop(key, None)
|
||||
|
||||
def lock(self, key: str, *, timeout: int, blocking: bool) -> "_FakeRedisLock":
|
||||
return _FakeRedisLock(self, key)
|
||||
|
||||
|
||||
class _FakeRedisLock:
|
||||
def __init__(self, redis: _FakeRedis, key: str) -> None:
|
||||
self._redis = redis
|
||||
self._key = key
|
||||
self._acquired = False
|
||||
|
||||
def acquire(self, *, blocking: bool) -> bool:
|
||||
if self._key in self._redis._values:
|
||||
return False
|
||||
|
||||
self._redis._values[self._key] = "locked"
|
||||
self._acquired = True
|
||||
return True
|
||||
|
||||
def release(self) -> None:
|
||||
if self._acquired:
|
||||
self._redis.delete(self._key)
|
||||
self._acquired = False
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_plugin_model_provider_memory_cache() -> None:
|
||||
@ -416,9 +439,10 @@ class TestPluginModelRuntime:
|
||||
mget=Mock(return_value=[None, None]),
|
||||
delete=Mock(),
|
||||
setex=Mock(),
|
||||
lock=Mock(return_value=SimpleNamespace(acquire=Mock(return_value=True), release=Mock())),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(plugin_service_module.dify_config, "PLUGIN_MODEL_PROVIDERS_CACHE_TTL", 300)
|
||||
monkeypatch.setattr(plugin_service_module.dify_config, "PLUGIN_MODEL_PROVIDERS_CACHE_TTL", 0)
|
||||
runtime = PluginModelRuntime(tenant_id="tenant", user_id="user", client=client, plugin_service=PluginService)
|
||||
|
||||
runtime.fetch_model_providers()
|
||||
|
||||
@ -235,6 +235,172 @@ class TestPluginModelProviderCache:
|
||||
client.fetch_model_providers.assert_called_once_with("tenant-1")
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
|
||||
def test_fetch_plugin_model_providers_waits_for_concurrent_refresh_cache_fill(self) -> None:
|
||||
"""A cache miss waits for the active tenant refresh instead of stampeding the daemon."""
|
||||
cached_provider = _build_provider_entity()
|
||||
cached_payload = TypeAdapter(list[ProviderEntity]).dump_json([cached_provider]).decode("utf-8")
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
legacy_cache_key = _provider_cache_key("tenant-1")
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}.redis_client") as redis_client,
|
||||
patch(f"{MODULE}.time.sleep") as sleep,
|
||||
):
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.side_effect = [[None, None], [cached_payload, None]]
|
||||
redis_client.lock.return_value.acquire.return_value = False
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [_build_plugin_model_provider(provider="anthropic")]
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
redis_client.lock.assert_called_once_with(
|
||||
PluginService._get_plugin_model_providers_lock_key("tenant-1", 0),
|
||||
timeout=PluginService.PLUGIN_MODEL_PROVIDERS_LOCK_TTL,
|
||||
blocking=False,
|
||||
)
|
||||
redis_client.lock.return_value.acquire.assert_called_once_with(blocking=False)
|
||||
assert redis_client.mget.call_args_list == [
|
||||
call([cache_key, legacy_cache_key]),
|
||||
call([cache_key, legacy_cache_key]),
|
||||
]
|
||||
sleep.assert_called()
|
||||
client.fetch_model_providers.assert_not_called()
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
|
||||
def test_fetch_plugin_model_providers_retries_lock_after_wait_timeout(self) -> None:
|
||||
"""Only a lock owner should refresh the daemon when the first refresh takes too long."""
|
||||
with (
|
||||
patch(f"{MODULE}.redis_client") as redis_client,
|
||||
patch(f"{MODULE}.time.sleep"),
|
||||
patch(f"{MODULE}.PluginService.PLUGIN_MODEL_PROVIDERS_LOCK_WAIT_TIMEOUT", 0),
|
||||
):
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.return_value = [None, None]
|
||||
redis_client.lock.return_value.acquire.side_effect = [False, True]
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [_build_plugin_model_provider()]
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert redis_client.lock.return_value.acquire.call_args_list == [
|
||||
call(blocking=False),
|
||||
call(blocking=False),
|
||||
]
|
||||
client.fetch_model_providers.assert_called_once_with("tenant-1")
|
||||
redis_client.lock.return_value.release.assert_called_once_with()
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
|
||||
def test_fetch_plugin_model_providers_releases_owned_refresh_lock_after_store(self) -> None:
|
||||
"""The refresh owner releases only its token after storing provider metadata."""
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
legacy_cache_key = _provider_cache_key("tenant-1")
|
||||
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.return_value = [None, None]
|
||||
redis_client.lock.return_value.acquire.return_value = True
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [_build_plugin_model_provider()]
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
redis_client.lock.assert_called_once_with(
|
||||
PluginService._get_plugin_model_providers_lock_key("tenant-1", 0),
|
||||
timeout=PluginService.PLUGIN_MODEL_PROVIDERS_LOCK_TTL,
|
||||
blocking=False,
|
||||
)
|
||||
redis_client.lock.return_value.acquire.assert_called_once_with(blocking=False)
|
||||
redis_client.mget.assert_called_once_with([cache_key, legacy_cache_key])
|
||||
redis_client.lock.return_value.release.assert_called_once_with()
|
||||
redis_client.eval.assert_not_called()
|
||||
client.fetch_model_providers.assert_called_once_with("tenant-1")
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
|
||||
def test_fetch_plugin_model_providers_skips_wait_when_refresh_lock_fails(self) -> None:
|
||||
"""Lock API failures should fall back directly instead of adding timeout latency."""
|
||||
with (
|
||||
patch(f"{MODULE}.redis_client") as redis_client,
|
||||
patch(f"{MODULE}.time.sleep") as sleep,
|
||||
):
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.return_value = [None, None]
|
||||
redis_client.lock.side_effect = RedisError("redis unavailable")
|
||||
redis_client.set.side_effect = AssertionError("raw redis set must not be used for refresh locks")
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [_build_plugin_model_provider()]
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
sleep.assert_not_called()
|
||||
redis_client.lock.assert_called_once()
|
||||
client.fetch_model_providers.assert_called_once_with("tenant-1")
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
|
||||
def test_fetch_plugin_model_providers_caches_empty_provider_list(self) -> None:
|
||||
"""An empty provider list is still a valid refresh result for single-flight waiters."""
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.return_value = [None, None]
|
||||
redis_client.lock.return_value.acquire.return_value = True
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = []
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert result == ()
|
||||
redis_client.setex.assert_called_once()
|
||||
assert redis_client.setex.call_args.args[0] == cache_key
|
||||
redis_client.lock.return_value.release.assert_called_once_with()
|
||||
|
||||
def test_fetch_plugin_model_providers_skips_cache_write_when_generation_changes_during_refresh(self) -> None:
|
||||
"""A refresh that started before invalidation must not populate the newer generation cache."""
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
redis_client.get.side_effect = [None, "1"]
|
||||
redis_client.mget.return_value = [None, None]
|
||||
redis_client.lock.return_value.acquire.return_value = True
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = []
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert result == ()
|
||||
client.fetch_model_providers.assert_called_once_with("tenant-1")
|
||||
redis_client.setex.assert_not_called()
|
||||
redis_client.lock.return_value.release.assert_called_once_with()
|
||||
|
||||
def test_fetch_plugin_model_providers_reuses_cached_empty_provider_list(self) -> None:
|
||||
"""A cached empty list should prevent repeated daemon fetches for tenants without plugin models."""
|
||||
empty_payload = TypeAdapter(list[ProviderEntity]).dump_json([]).decode("utf-8")
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
legacy_cache_key = _provider_cache_key("tenant-1")
|
||||
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
redis_client.get.return_value = None
|
||||
redis_client.mget.return_value = [empty_payload, None]
|
||||
client = Mock()
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert result == ()
|
||||
redis_client.mget.assert_called_once_with([cache_key, legacy_cache_key])
|
||||
client.fetch_model_providers.assert_not_called()
|
||||
|
||||
def test_fetch_plugin_model_providers_creates_default_client_on_cache_miss(self) -> None:
|
||||
"""The service owns plugin daemon access when no runtime-provided client is injected."""
|
||||
with (
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
@agent-v2 @authenticated @access-point @core
|
||||
Feature: Agent v2 Access Point
|
||||
Scenario: Access Point shows the available Agent v2 access surfaces
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
When I open the Agent v2 Access Point page
|
||||
Then I should see the Agent v2 Access Point overview
|
||||
|
||||
Scenario: Backend service API supports endpoint copy, key creation, and API reference navigation
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
And Agent v2 Backend service API access has been enabled via API
|
||||
When I open the Agent v2 configure page from the Agent Roster
|
||||
And I switch to the Agent v2 Access Point section
|
||||
Then I should see the Agent v2 Backend service API endpoint
|
||||
When I copy the Agent v2 Backend service API endpoint
|
||||
Then the Agent v2 Backend service API endpoint should show it was copied
|
||||
When I open Agent v2 API key management
|
||||
Then Agent v2 API keys should not expose a secret by default
|
||||
When I create a new Agent v2 API key
|
||||
Then I should see the newly generated Agent v2 API key once
|
||||
When I close the newly generated Agent v2 API key
|
||||
Then the Agent v2 API key list should not expose the full generated secret
|
||||
When I close Agent v2 API key management
|
||||
And I open the Agent v2 API Reference
|
||||
Then the Agent v2 API Reference should open in a new tab
|
||||
@ -1,16 +0,0 @@
|
||||
@agent-v2 @authenticated @build @core
|
||||
Feature: Agent v2 build draft
|
||||
Scenario: Discarding a Build draft keeps the original Agent configuration
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
And the Agent v2 composer draft uses the normal E2E prompt
|
||||
And an Agent v2 Build draft uses the updated E2E prompt
|
||||
When I open the Agent v2 configure page
|
||||
Then I should see the Agent v2 Build draft pending changes
|
||||
And I should see the updated E2E prompt in the Agent v2 prompt editor
|
||||
When I discard the Agent v2 Build draft
|
||||
Then I should see the normal E2E prompt in the Agent v2 prompt editor
|
||||
And the Agent v2 Build draft should no longer be active
|
||||
When I refresh the current page
|
||||
Then I should see the normal E2E prompt in the Agent v2 prompt editor
|
||||
And the Agent v2 Build draft should no longer be active
|
||||
@ -1,10 +0,0 @@
|
||||
@agent-v2 @authenticated @core
|
||||
Feature: Agent v2 configure persistence
|
||||
Scenario: Persisted Agent v2 instructions remain visible after refresh
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
And the Agent v2 composer draft uses the normal E2E prompt
|
||||
When I open the Agent v2 configure page
|
||||
Then I should see the normal E2E prompt in the Agent v2 prompt editor
|
||||
When I refresh the current page
|
||||
Then I should see the normal E2E prompt in the Agent v2 prompt editor
|
||||
@ -1,9 +0,0 @@
|
||||
@agent-v2 @authenticated @core
|
||||
Feature: Agent v2 configure validation
|
||||
Scenario: Preview is unavailable until a required model is configured
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
And the Agent v2 composer draft uses the normal E2E prompt
|
||||
When I open the Agent v2 configure page
|
||||
Then Agent v2 Preview should be unavailable until a model is configured
|
||||
And I should see the normal E2E prompt in the Agent v2 prompt editor
|
||||
@ -1,9 +0,0 @@
|
||||
@agent-v2 @authenticated @publish @core
|
||||
Feature: Agent v2 publish
|
||||
Scenario: Publish a configured Agent v2 draft
|
||||
Given I am signed in as the default E2E admin
|
||||
And an Agent v2 test agent has been created via API
|
||||
And the Agent v2 composer draft uses the normal E2E prompt
|
||||
When I open the Agent v2 configure page
|
||||
And I publish the Agent v2 draft
|
||||
Then the Agent v2 draft should be published and up to date
|
||||
@ -1,175 +0,0 @@
|
||||
import type { DifyWorld } from '../../support/world'
|
||||
import { Given, Then, When } from '@cucumber/cucumber'
|
||||
import { expect } from '@playwright/test'
|
||||
import { getAgentAccessPath, setAgentApiAccess } from '../../../support/agent'
|
||||
|
||||
const getCurrentAgentId = (world: DifyWorld) => {
|
||||
const agentId = world.createdAgentIds.at(-1)
|
||||
if (!agentId)
|
||||
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
|
||||
|
||||
return agentId
|
||||
}
|
||||
|
||||
Given(
|
||||
'Agent v2 Backend service API access has been enabled via API',
|
||||
async function (this: DifyWorld) {
|
||||
const apiAccess = await setAgentApiAccess(getCurrentAgentId(this), true)
|
||||
|
||||
this.lastAgentServiceApiBaseURL = apiAccess.service_api_base_url
|
||||
},
|
||||
)
|
||||
|
||||
When('I open the Agent v2 Access Point page', async function (this: DifyWorld) {
|
||||
await this.getPage().goto(getAgentAccessPath(getCurrentAgentId(this)))
|
||||
})
|
||||
|
||||
When('I switch to the Agent v2 Access Point section', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const agentId = getCurrentAgentId(this)
|
||||
|
||||
await page.getByRole('link', { name: 'Access Point' }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/roster/agent/${agentId}/access(?:\\?.*)?$`))
|
||||
await expect(page.getByRole('region', { name: 'Access Point' })).toBeVisible()
|
||||
})
|
||||
|
||||
Then('I should see the Agent v2 Access Point overview', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const accessRegion = page.getByRole('region', { name: 'Access Point' })
|
||||
|
||||
await expect(accessRegion).toBeVisible({ timeout: 30_000 })
|
||||
await expect(accessRegion.getByRole('heading', { name: 'Access Point' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('heading', { name: 'Web app' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('heading', { name: 'Backend service API' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('heading', { name: 'Workflow access' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('columnheader', { name: 'Name' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('columnheader', { name: 'Version' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('columnheader', { name: 'Nodes' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('columnheader', { name: 'Last updated' })).toBeVisible()
|
||||
await expect(accessRegion.getByRole('columnheader', { name: 'Actions' })).toBeVisible()
|
||||
await expect(accessRegion.getByText('No workflow references yet.')).toBeVisible()
|
||||
})
|
||||
|
||||
Then('I should see the Agent v2 Backend service API endpoint', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
if (!this.lastAgentServiceApiBaseURL)
|
||||
throw new Error('No Agent v2 service API endpoint found. Enable Backend service API first.')
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Backend service API' })).toBeVisible({
|
||||
timeout: 30_000,
|
||||
})
|
||||
await expect(page.getByText('Service API Endpoint')).toBeVisible()
|
||||
await expect(page.getByText(this.lastAgentServiceApiBaseURL)).toBeVisible()
|
||||
await expect(page.getByLabel('Copy service API endpoint')).toBeEnabled()
|
||||
})
|
||||
|
||||
When('I copy the Agent v2 Backend service API endpoint', async function (this: DifyWorld) {
|
||||
await this.getPage().getByLabel('Copy service API endpoint').click()
|
||||
})
|
||||
|
||||
Then(
|
||||
'the Agent v2 Backend service API endpoint should show it was copied',
|
||||
async function (this: DifyWorld) {
|
||||
await expect(this.getPage().getByLabel('Copied')).toBeVisible()
|
||||
},
|
||||
)
|
||||
|
||||
When('I open Agent v2 API key management', async function (this: DifyWorld) {
|
||||
await this.getPage()
|
||||
.getByRole('button', { name: /^API Key\b/ })
|
||||
.click()
|
||||
})
|
||||
|
||||
Then('Agent v2 API keys should not expose a secret by default', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const dialog = page.getByRole('dialog', { name: /API Secret key/i })
|
||||
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByText('Secret Key', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByText('CREATED', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByText('LAST USED', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByText('No data', { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByRole('button', { name: 'Create new Secret key' })).toBeVisible()
|
||||
await expect(dialog.getByText(/^app-/)).not.toBeVisible()
|
||||
await expect(page.getByRole('dialog', { name: 'Internal Server Error' })).not.toBeVisible()
|
||||
})
|
||||
|
||||
When('I create a new Agent v2 API key', async function (this: DifyWorld) {
|
||||
const dialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
|
||||
|
||||
await dialog.getByRole('button', { name: 'Create new Secret key' }).click()
|
||||
})
|
||||
|
||||
Then('I should see the newly generated Agent v2 API key once', async function (this: DifyWorld) {
|
||||
const generatedKeyDialog = this.getPage()
|
||||
.getByRole('dialog', { name: /API Secret key/i })
|
||||
.last()
|
||||
const generatedKey = generatedKeyDialog.getByText(/^app-/)
|
||||
|
||||
await expect(generatedKeyDialog).toBeVisible()
|
||||
await expect(
|
||||
generatedKeyDialog.getByText('Keep this key in a secure and accessible place.'),
|
||||
).toBeVisible()
|
||||
await expect(generatedKey).toBeVisible()
|
||||
await expect(generatedKeyDialog.getByLabel('Copy')).toBeVisible()
|
||||
|
||||
this.lastGeneratedAgentApiKey = (await generatedKey.textContent())?.trim()
|
||||
if (!this.lastGeneratedAgentApiKey)
|
||||
throw new Error('Generated Agent v2 API key was empty.')
|
||||
})
|
||||
|
||||
When('I close the newly generated Agent v2 API key', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const generatedKeyDialog = page.getByRole('dialog', { name: /API Secret key/i }).last()
|
||||
|
||||
await generatedKeyDialog.getByRole('button', { name: 'OK' }).click()
|
||||
await expect(page.getByText('Keep this key in a secure and accessible place.')).not.toBeVisible()
|
||||
})
|
||||
|
||||
Then(
|
||||
'the Agent v2 API key list should not expose the full generated secret',
|
||||
async function (this: DifyWorld) {
|
||||
const fullSecret = this.lastGeneratedAgentApiKey
|
||||
if (!fullSecret)
|
||||
throw new Error('No generated Agent v2 API key found.')
|
||||
|
||||
const apiKeyDialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
|
||||
|
||||
await expect(apiKeyDialog).toBeVisible()
|
||||
await expect(apiKeyDialog.getByText(fullSecret, { exact: true })).not.toBeVisible()
|
||||
await expect(apiKeyDialog.getByText(/^app-/)).not.toBeVisible()
|
||||
await expect(apiKeyDialog.getByLabel('Copy')).toBeVisible()
|
||||
},
|
||||
)
|
||||
|
||||
When('I close Agent v2 API key management', async function (this: DifyWorld) {
|
||||
const apiKeyDialog = this.getPage().getByRole('dialog', { name: /API Secret key/i })
|
||||
|
||||
await apiKeyDialog.getByLabel('Close').click()
|
||||
await expect(apiKeyDialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
When('I open the Agent v2 API Reference', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const apiReferenceLink = page.getByRole('link', { name: 'API Reference' })
|
||||
|
||||
await expect(apiReferenceLink).toBeVisible()
|
||||
|
||||
const [apiReferencePage] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
apiReferenceLink.click(),
|
||||
])
|
||||
|
||||
this.lastAgentApiReferencePage = apiReferencePage
|
||||
})
|
||||
|
||||
Then('the Agent v2 API Reference should open in a new tab', async function (this: DifyWorld) {
|
||||
const apiReferencePage = this.lastAgentApiReferencePage
|
||||
if (!apiReferencePage)
|
||||
throw new Error('No Agent v2 API Reference page was opened.')
|
||||
|
||||
await expect(apiReferencePage).toHaveURL(/developing-with-apis/)
|
||||
await apiReferencePage.close()
|
||||
this.lastAgentApiReferencePage = undefined
|
||||
})
|
||||
@ -4,23 +4,9 @@ import { expect } from '@playwright/test'
|
||||
import {
|
||||
createTestAgent,
|
||||
getAgentConfigurePath,
|
||||
getTestAgent,
|
||||
normalAgentPrompt,
|
||||
normalAgentSoulConfig,
|
||||
saveAgentBuildDraft,
|
||||
saveAgentComposerDraft,
|
||||
updatedAgentPrompt,
|
||||
updatedAgentSoulConfig,
|
||||
} from '../../../support/agent'
|
||||
|
||||
const getCurrentAgentId = (world: DifyWorld) => {
|
||||
const agentId = world.createdAgentIds.at(-1)
|
||||
if (!agentId)
|
||||
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
|
||||
|
||||
return agentId
|
||||
}
|
||||
|
||||
Given('an Agent v2 test agent has been created via API', async function (this: DifyWorld) {
|
||||
const agent = await createTestAgent()
|
||||
this.createdAgentIds.push(agent.id)
|
||||
@ -29,53 +15,25 @@ Given('an Agent v2 test agent has been created via API', async function (this: D
|
||||
})
|
||||
|
||||
Given('a minimal Agent v2 composer draft has been synced', async function (this: DifyWorld) {
|
||||
const agentId = getCurrentAgentId(this)
|
||||
const agentId = this.createdAgentIds.at(-1)
|
||||
if (!agentId)
|
||||
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
|
||||
|
||||
await saveAgentComposerDraft(agentId)
|
||||
})
|
||||
|
||||
Given('the Agent v2 composer draft uses the normal E2E prompt', async function (this: DifyWorld) {
|
||||
await saveAgentComposerDraft(getCurrentAgentId(this), normalAgentSoulConfig)
|
||||
})
|
||||
|
||||
Given('an Agent v2 Build draft uses the updated E2E prompt', async function (this: DifyWorld) {
|
||||
await saveAgentBuildDraft(getCurrentAgentId(this), updatedAgentSoulConfig)
|
||||
})
|
||||
|
||||
When('I open the Agent v2 configure page', async function (this: DifyWorld) {
|
||||
await this.getPage().goto(getAgentConfigurePath(getCurrentAgentId(this)))
|
||||
})
|
||||
const agentId = this.createdAgentIds.at(-1)
|
||||
if (!agentId)
|
||||
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
|
||||
|
||||
When(
|
||||
'I open the Agent v2 configure page from the Agent Roster',
|
||||
async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const agentId = getCurrentAgentId(this)
|
||||
const agentName = this.lastCreatedAgentName
|
||||
if (!agentName)
|
||||
throw new Error('No Agent v2 name found. Create an Agent v2 test agent first.')
|
||||
|
||||
await page.goto('/roster')
|
||||
await page.getByRole('link', { name: agentName }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/roster/agent/${agentId}/configure(?:\\?.*)?$`))
|
||||
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
|
||||
},
|
||||
)
|
||||
|
||||
When('I discard the Agent v2 Build draft', async function (this: DifyWorld) {
|
||||
await this.getPage().getByRole('button', { name: 'Discard' }).click()
|
||||
})
|
||||
|
||||
When('I publish the Agent v2 draft', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const publishButton = page.getByRole('button', { name: /^Publish(?: update)?$/ })
|
||||
|
||||
await expect(publishButton).toBeEnabled({ timeout: 30_000 })
|
||||
await publishButton.click()
|
||||
await this.getPage().goto(getAgentConfigurePath(agentId))
|
||||
})
|
||||
|
||||
Then('I should be on the Agent v2 configure page', async function (this: DifyWorld) {
|
||||
const agentId = getCurrentAgentId(this)
|
||||
const agentId = this.createdAgentIds.at(-1)
|
||||
if (!agentId)
|
||||
throw new Error('No Agent v2 ID found. Create an Agent v2 test agent first.')
|
||||
|
||||
await expect(this.getPage()).toHaveURL(
|
||||
new RegExp(`/roster/agent/${agentId}/configure(?:\\?.*)?$`),
|
||||
@ -89,58 +47,3 @@ Then('I should see the Agent v2 configure workspace', async function (this: Dify
|
||||
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible()
|
||||
await expect(page.getByText(this.lastCreatedAgentName!)).toBeVisible()
|
||||
})
|
||||
|
||||
Then(
|
||||
'I should see the normal E2E prompt in the Agent v2 prompt editor',
|
||||
async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Prompt' })).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByText(normalAgentPrompt)).toBeVisible()
|
||||
},
|
||||
)
|
||||
|
||||
Then(
|
||||
'I should see the updated E2E prompt in the Agent v2 prompt editor',
|
||||
async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Prompt' })).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByText(updatedAgentPrompt)).toBeVisible()
|
||||
},
|
||||
)
|
||||
|
||||
Then(
|
||||
'Agent v2 Preview should be unavailable until a model is configured',
|
||||
async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Configure' })).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByRole('button', { name: /^Preview$/i })).toBeDisabled()
|
||||
},
|
||||
)
|
||||
|
||||
Then('I should see the Agent v2 Build draft pending changes', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await expect(page.getByText('Build draft')).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByRole('button', { name: 'Apply' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Discard' })).toBeVisible()
|
||||
})
|
||||
|
||||
Then('the Agent v2 Build draft should no longer be active', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await expect(page.getByText('Build draft')).not.toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Apply' })).not.toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Discard' })).not.toBeVisible()
|
||||
})
|
||||
|
||||
Then('the Agent v2 draft should be published and up to date', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const agentId = getCurrentAgentId(this)
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Published' })).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByText('Up to date')).toBeVisible()
|
||||
await expect.poll(async () => (await getTestAgent(agentId)).active_config_is_published).toBe(true)
|
||||
})
|
||||
|
||||
@ -7,10 +7,6 @@ When('I open the apps console', async function (this: DifyWorld) {
|
||||
await this.getPage().goto('/apps')
|
||||
})
|
||||
|
||||
When('I refresh the current page', async function (this: DifyWorld) {
|
||||
await this.getPage().reload()
|
||||
})
|
||||
|
||||
Then('I should stay on the apps console', async function (this: DifyWorld) {
|
||||
await waitForAppsConsole(this.getPage())
|
||||
})
|
||||
|
||||
@ -15,9 +15,6 @@ export class DifyWorld extends World {
|
||||
lastCreatedAppName: string | undefined
|
||||
lastCreatedAgentName: string | undefined
|
||||
lastCreatedAgentRole: string | undefined
|
||||
lastAgentServiceApiBaseURL: string | undefined
|
||||
lastGeneratedAgentApiKey: string | undefined
|
||||
lastAgentApiReferencePage: Page | undefined
|
||||
createdAppIds: string[] = []
|
||||
createdAgentIds: string[] = []
|
||||
capturedDownloads: Download[] = []
|
||||
@ -34,9 +31,6 @@ export class DifyWorld extends World {
|
||||
this.lastCreatedAppName = undefined
|
||||
this.lastCreatedAgentName = undefined
|
||||
this.lastCreatedAgentRole = undefined
|
||||
this.lastAgentServiceApiBaseURL = undefined
|
||||
this.lastGeneratedAgentApiKey = undefined
|
||||
this.lastAgentApiReferencePage = undefined
|
||||
this.createdAppIds = []
|
||||
this.createdAgentIds = []
|
||||
this.capturedDownloads = []
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { createApiContext, expectApiResponseOK, setAppSiteEnabled } from './api'
|
||||
|
||||
export type AgentSeed = {
|
||||
active_config_is_published?: boolean
|
||||
app_id?: string
|
||||
backing_app_id?: string
|
||||
description?: string
|
||||
@ -30,9 +29,10 @@ export type AgentBuildDraftResponse = {
|
||||
|
||||
export type AgentApiAccess = {
|
||||
api_key_count: number
|
||||
api_reference_url: string
|
||||
endpoint: string
|
||||
enabled: boolean
|
||||
files_upload_endpoint: string
|
||||
service_api_base_url: string
|
||||
}
|
||||
|
||||
export type AgentApiKey = {
|
||||
@ -46,24 +46,6 @@ export const defaultAgentSoulConfig: AgentSoulConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
export const normalAgentPrompt
|
||||
= 'You are a Dify Agent E2E test assistant. Reply briefly to every user message, and always include AGENT_E2E_PASS in your response.'
|
||||
|
||||
export const updatedAgentPrompt
|
||||
= 'You are a Dify Agent E2E test assistant. Every response must start with E2E_AGENT_UPDATED.'
|
||||
|
||||
export const normalAgentSoulConfig: AgentSoulConfig = {
|
||||
prompt: {
|
||||
system_prompt: normalAgentPrompt,
|
||||
},
|
||||
}
|
||||
|
||||
export const updatedAgentSoulConfig: AgentSoulConfig = {
|
||||
prompt: {
|
||||
system_prompt: updatedAgentPrompt,
|
||||
},
|
||||
}
|
||||
|
||||
export const getAgentConfigurePath = (agentId: string) => `/roster/agent/${agentId}/configure`
|
||||
export const getAgentAccessPath = (agentId: string) => `/roster/agent/${agentId}/access`
|
||||
|
||||
@ -154,27 +136,6 @@ export async function checkoutAgentBuildDraft(agentId: string): Promise<AgentBui
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAgentBuildDraft(
|
||||
agentId: string,
|
||||
agentSoul: AgentSoulConfig,
|
||||
): Promise<AgentBuildDraftResponse> {
|
||||
const ctx = await createApiContext()
|
||||
try {
|
||||
const response = await ctx.put(`/console/api/agent/${agentId}/build-draft`, {
|
||||
data: {
|
||||
agent_soul: agentSoul,
|
||||
save_strategy: 'save_to_current_version',
|
||||
variant: 'agent_app',
|
||||
},
|
||||
})
|
||||
await expectApiResponseOK(response, `Save Agent v2 build draft for ${agentId}`)
|
||||
return (await response.json()) as AgentBuildDraftResponse
|
||||
}
|
||||
finally {
|
||||
await ctx.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export async function discardAgentBuildDraft(agentId: string): Promise<void> {
|
||||
const ctx = await createApiContext()
|
||||
try {
|
||||
|
||||
@ -1,103 +1,54 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { account } from './account/orpc.gen'
|
||||
import { activate } from './activate/orpc.gen'
|
||||
import { agent } from './agent/orpc.gen'
|
||||
import { allWorkspaces } from './all-workspaces/orpc.gen'
|
||||
import { apiBasedExtension } from './api-based-extension/orpc.gen'
|
||||
import { apiKeyAuth } from './api-key-auth/orpc.gen'
|
||||
import { appDslVersion } from './app-dsl-version/orpc.gen'
|
||||
import { app } from './app/orpc.gen'
|
||||
import { apps } from './apps/orpc.gen'
|
||||
import { auth } from './auth/orpc.gen'
|
||||
import { billing } from './billing/orpc.gen'
|
||||
import { codeBasedExtension } from './code-based-extension/orpc.gen'
|
||||
import { compliance } from './compliance/orpc.gen'
|
||||
import { dataSource } from './data-source/orpc.gen'
|
||||
import { datasets } from './datasets/orpc.gen'
|
||||
import { emailCodeLogin } from './email-code-login/orpc.gen'
|
||||
import { emailRegister } from './email-register/orpc.gen'
|
||||
import { explore } from './explore/orpc.gen'
|
||||
import { features } from './features/orpc.gen'
|
||||
import { files } from './files/orpc.gen'
|
||||
import { forgotPassword } from './forgot-password/orpc.gen'
|
||||
import { form } from './form/orpc.gen'
|
||||
import { info } from './info/orpc.gen'
|
||||
import { installedApps } from './installed-apps/orpc.gen'
|
||||
import { instructionGenerate } from './instruction-generate/orpc.gen'
|
||||
import { login } from './login/orpc.gen'
|
||||
import { logout } from './logout/orpc.gen'
|
||||
import { notification } from './notification/orpc.gen'
|
||||
import { notion } from './notion/orpc.gen'
|
||||
import { oauth } from './oauth/orpc.gen'
|
||||
import { rag } from './rag/orpc.gen'
|
||||
import { refreshToken } from './refresh-token/orpc.gen'
|
||||
import { remoteFiles } from './remote-files/orpc.gen'
|
||||
import { resetPassword } from './reset-password/orpc.gen'
|
||||
import { ruleCodeGenerate } from './rule-code-generate/orpc.gen'
|
||||
import { ruleGenerate } from './rule-generate/orpc.gen'
|
||||
import { ruleStructuredOutputGenerate } from './rule-structured-output-generate/orpc.gen'
|
||||
import { snippets } from './snippets/orpc.gen'
|
||||
import { spec } from './spec/orpc.gen'
|
||||
import { systemFeatures } from './system-features/orpc.gen'
|
||||
import { tagBindings } from './tag-bindings/orpc.gen'
|
||||
import { tags } from './tags/orpc.gen'
|
||||
import { test } from './test/orpc.gen'
|
||||
import { trialApps } from './trial-apps/orpc.gen'
|
||||
import { trialModels } from './trial-models/orpc.gen'
|
||||
import { website } from './website/orpc.gen'
|
||||
import { workflowGenerate } from './workflow-generate/orpc.gen'
|
||||
import { workflow } from './workflow/orpc.gen'
|
||||
import { workspaces } from './workspaces/orpc.gen'
|
||||
|
||||
export const contract = {
|
||||
account,
|
||||
activate,
|
||||
agent,
|
||||
allWorkspaces,
|
||||
apiBasedExtension,
|
||||
apiKeyAuth,
|
||||
app,
|
||||
appDslVersion,
|
||||
apps,
|
||||
auth,
|
||||
billing,
|
||||
codeBasedExtension,
|
||||
compliance,
|
||||
dataSource,
|
||||
datasets,
|
||||
emailCodeLogin,
|
||||
emailRegister,
|
||||
explore,
|
||||
features,
|
||||
files,
|
||||
forgotPassword,
|
||||
form,
|
||||
info,
|
||||
installedApps,
|
||||
instructionGenerate,
|
||||
login,
|
||||
logout,
|
||||
notification,
|
||||
notion,
|
||||
oauth,
|
||||
rag,
|
||||
refreshToken,
|
||||
remoteFiles,
|
||||
resetPassword,
|
||||
ruleCodeGenerate,
|
||||
ruleGenerate,
|
||||
ruleStructuredOutputGenerate,
|
||||
snippets,
|
||||
spec,
|
||||
systemFeatures,
|
||||
tagBindings,
|
||||
tags,
|
||||
test,
|
||||
trialApps,
|
||||
trialModels,
|
||||
website,
|
||||
workflow,
|
||||
workflowGenerate,
|
||||
workspaces,
|
||||
export const contractLoaders = {
|
||||
account: () => import('./account/orpc.gen').then(({ account }) => ({ account })),
|
||||
activate: () => import('./activate/orpc.gen').then(({ activate }) => ({ activate })),
|
||||
agent: () => import('./agent/orpc.gen').then(({ agent }) => ({ agent })),
|
||||
allWorkspaces: () => import('./all-workspaces/orpc.gen').then(({ allWorkspaces }) => ({ allWorkspaces })),
|
||||
apiBasedExtension: () => import('./api-based-extension/orpc.gen').then(({ apiBasedExtension }) => ({ apiBasedExtension })),
|
||||
apiKeyAuth: () => import('./api-key-auth/orpc.gen').then(({ apiKeyAuth }) => ({ apiKeyAuth })),
|
||||
app: () => import('./app/orpc.gen').then(({ app }) => ({ app })),
|
||||
appDslVersion: () => import('./app-dsl-version/orpc.gen').then(({ appDslVersion }) => ({ appDslVersion })),
|
||||
apps: () => import('./apps/orpc.gen').then(({ apps }) => ({ apps })),
|
||||
auth: () => import('./auth/orpc.gen').then(({ auth }) => ({ auth })),
|
||||
billing: () => import('./billing/orpc.gen').then(({ billing }) => ({ billing })),
|
||||
codeBasedExtension: () => import('./code-based-extension/orpc.gen').then(({ codeBasedExtension }) => ({ codeBasedExtension })),
|
||||
compliance: () => import('./compliance/orpc.gen').then(({ compliance }) => ({ compliance })),
|
||||
dataSource: () => import('./data-source/orpc.gen').then(({ dataSource }) => ({ dataSource })),
|
||||
datasets: () => import('./datasets/orpc.gen').then(({ datasets }) => ({ datasets })),
|
||||
emailCodeLogin: () => import('./email-code-login/orpc.gen').then(({ emailCodeLogin }) => ({ emailCodeLogin })),
|
||||
emailRegister: () => import('./email-register/orpc.gen').then(({ emailRegister }) => ({ emailRegister })),
|
||||
explore: () => import('./explore/orpc.gen').then(({ explore }) => ({ explore })),
|
||||
features: () => import('./features/orpc.gen').then(({ features }) => ({ features })),
|
||||
files: () => import('./files/orpc.gen').then(({ files }) => ({ files })),
|
||||
forgotPassword: () => import('./forgot-password/orpc.gen').then(({ forgotPassword }) => ({ forgotPassword })),
|
||||
form: () => import('./form/orpc.gen').then(({ form }) => ({ form })),
|
||||
info: () => import('./info/orpc.gen').then(({ info }) => ({ info })),
|
||||
installedApps: () => import('./installed-apps/orpc.gen').then(({ installedApps }) => ({ installedApps })),
|
||||
instructionGenerate: () => import('./instruction-generate/orpc.gen').then(({ instructionGenerate }) => ({ instructionGenerate })),
|
||||
login: () => import('./login/orpc.gen').then(({ login }) => ({ login })),
|
||||
logout: () => import('./logout/orpc.gen').then(({ logout }) => ({ logout })),
|
||||
notification: () => import('./notification/orpc.gen').then(({ notification }) => ({ notification })),
|
||||
notion: () => import('./notion/orpc.gen').then(({ notion }) => ({ notion })),
|
||||
oauth: () => import('./oauth/orpc.gen').then(({ oauth }) => ({ oauth })),
|
||||
rag: () => import('./rag/orpc.gen').then(({ rag }) => ({ rag })),
|
||||
refreshToken: () => import('./refresh-token/orpc.gen').then(({ refreshToken }) => ({ refreshToken })),
|
||||
remoteFiles: () => import('./remote-files/orpc.gen').then(({ remoteFiles }) => ({ remoteFiles })),
|
||||
resetPassword: () => import('./reset-password/orpc.gen').then(({ resetPassword }) => ({ resetPassword })),
|
||||
ruleCodeGenerate: () => import('./rule-code-generate/orpc.gen').then(({ ruleCodeGenerate }) => ({ ruleCodeGenerate })),
|
||||
ruleGenerate: () => import('./rule-generate/orpc.gen').then(({ ruleGenerate }) => ({ ruleGenerate })),
|
||||
ruleStructuredOutputGenerate: () =>
|
||||
import('./rule-structured-output-generate/orpc.gen').then(({ ruleStructuredOutputGenerate }) => ({ ruleStructuredOutputGenerate })),
|
||||
snippets: () => import('./snippets/orpc.gen').then(({ snippets }) => ({ snippets })),
|
||||
spec: () => import('./spec/orpc.gen').then(({ spec }) => ({ spec })),
|
||||
systemFeatures: () => import('./system-features/orpc.gen').then(({ systemFeatures }) => ({ systemFeatures })),
|
||||
tagBindings: () => import('./tag-bindings/orpc.gen').then(({ tagBindings }) => ({ tagBindings })),
|
||||
tags: () => import('./tags/orpc.gen').then(({ tags }) => ({ tags })),
|
||||
test: () => import('./test/orpc.gen').then(({ test }) => ({ test })),
|
||||
trialApps: () => import('./trial-apps/orpc.gen').then(({ trialApps }) => ({ trialApps })),
|
||||
trialModels: () => import('./trial-models/orpc.gen').then(({ trialModels }) => ({ trialModels })),
|
||||
website: () => import('./website/orpc.gen').then(({ website }) => ({ website })),
|
||||
workflow: () => import('./workflow/orpc.gen').then(({ workflow }) => ({ workflow })),
|
||||
workflowGenerate: () => import('./workflow-generate/orpc.gen').then(({ workflowGenerate }) => ({ workflowGenerate })),
|
||||
workspaces: () => import('./workspaces/orpc.gen').then(({ workspaces }) => ({ workspaces })),
|
||||
}
|
||||
|
||||
@ -344,16 +344,13 @@ const consoleContractEntryContent = (segments: string[]) => {
|
||||
}
|
||||
})
|
||||
|
||||
const imports = contracts
|
||||
.map(contract => `import { ${contract.name} } from './${contract.importPath}/orpc.gen'`)
|
||||
const contractEntries = contracts
|
||||
.map(contract => ` ${contract.name}: () => import('./${contract.importPath}/orpc.gen').then(({ ${contract.name} }) => ({ ${contract.name} })),`)
|
||||
.join('\n')
|
||||
const contractEntries = contracts.map(contract => ` ${contract.name},`).join('\n')
|
||||
|
||||
return `// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
${imports}
|
||||
|
||||
export const contract = {
|
||||
export const contractLoaders = {
|
||||
${contractEntries}
|
||||
}
|
||||
`
|
||||
|
||||
21
web/app/(commonLayout)/global-mounts.tsx
Normal file
21
web/app/(commonLayout)/global-mounts.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from '@/next/dynamic'
|
||||
|
||||
const InSiteMessageNotification = dynamic(() => import('@/app/components/app/in-site-message/notification'), { ssr: false })
|
||||
const PartnerStack = dynamic(() => import('@/app/components/billing/partner-stack'), { ssr: false })
|
||||
const ReadmePanel = dynamic(() => import('@/app/components/plugins/readme-panel'), { ssr: false })
|
||||
const WorkflowGeneratorMount = dynamic(() => import('@/app/components/workflow/workflow-generator/mount'), { ssr: false })
|
||||
const GotoAnything = dynamic(() => import('@/app/components/goto-anything').then(mod => mod.GotoAnything), { ssr: false })
|
||||
|
||||
export function CommonLayoutGlobalMounts() {
|
||||
return (
|
||||
<>
|
||||
<InSiteMessageNotification />
|
||||
<PartnerStack />
|
||||
<ReadmePanel />
|
||||
<GotoAnything />
|
||||
<WorkflowGeneratorMount />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,20 +1,16 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import InSiteMessageNotification from '@/app/components/app/in-site-message/notification'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
|
||||
import { GotoAnything } from '@/app/components/goto-anything'
|
||||
import MainNavLayout from '@/app/components/main-nav/layout'
|
||||
import { NextRouteStateBridge } from '@/app/components/next-route-state'
|
||||
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
|
||||
import ReadmePanel from '@/app/components/plugins/readme-panel'
|
||||
import WorkflowGeneratorMount from '@/app/components/workflow/workflow-generator/mount'
|
||||
import { AppContextProvider } from '@/context/app-context-provider'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||
import PartnerStack from '../components/billing/partner-stack'
|
||||
import { CommonLayoutGlobalMounts } from './global-mounts'
|
||||
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
|
||||
|
||||
export default async function Layout({ children }: { children: ReactNode }) {
|
||||
@ -33,11 +29,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
|
||||
<MainNavLayout>
|
||||
{children}
|
||||
</MainNavLayout>
|
||||
<InSiteMessageNotification />
|
||||
<PartnerStack />
|
||||
<ReadmePanel />
|
||||
<GotoAnything />
|
||||
<WorkflowGeneratorMount />
|
||||
<CommonLayoutGlobalMounts />
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
|
||||
@ -495,7 +495,7 @@ describe('MainNav', () => {
|
||||
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('aligns the global navigation spacing with the main sidebar design', () => {
|
||||
it('aligns the global navigation spacing with the main sidebar design', async () => {
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
|
||||
renderMainNav()
|
||||
@ -508,7 +508,7 @@ describe('MainNav', () => {
|
||||
expect(homeLink.closest('nav')).toHaveClass('isolate', 'flex', 'flex-col', 'gap-px', 'p-2')
|
||||
expect(homeLink).toHaveClass('h-8', 'w-full', 'rounded-[10px]', 'px-2', 'py-1.5')
|
||||
|
||||
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
|
||||
const webAppsButton = await screen.findByRole('button', { name: 'explore.sidebar.webApps' })
|
||||
expect(webAppsButton.parentElement).toHaveClass('py-1', 'pr-2', 'pl-2')
|
||||
|
||||
const helpButton = screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })
|
||||
@ -551,7 +551,7 @@ describe('MainNav', () => {
|
||||
expect(container.querySelector('.relative.z-30')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the environment tag when app detail navigation is collapsed', () => {
|
||||
it('hides the environment tag when app detail navigation is collapsed', async () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
...appContextValue,
|
||||
@ -562,7 +562,7 @@ describe('MainNav', () => {
|
||||
})
|
||||
|
||||
const { container } = renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('app-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
|
||||
|
||||
expect(screen.queryByText('common.environment.testing')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('.relative.z-30')).not.toBeInTheDocument()
|
||||
@ -659,7 +659,7 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
|
||||
it('replaces global navigation with snippet detail navigation on snippet routes', () => {
|
||||
it('replaces global navigation with snippet detail navigation on snippet routes', async () => {
|
||||
mockPathname = '/snippets/snippet-1/orchestrate'
|
||||
snippetDraftState.inputFields = snippetFields
|
||||
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
|
||||
@ -671,8 +671,8 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-62')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
|
||||
expect(screen.getByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('snippet-sidebar-content')).toHaveAttribute('data-readonly', 'false')
|
||||
expect(await screen.findByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(await screen.findByTestId('snippet-sidebar-content')).toHaveAttribute('data-readonly', 'false')
|
||||
expect(screen.getByText(snippet.name)).toBeInTheDocument()
|
||||
expect(screen.getByText('query')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'change snippet fields' }))
|
||||
@ -686,14 +686,14 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses snippet detail navigation from the top-right toggle', () => {
|
||||
it('collapses snippet detail navigation from the top-right toggle', async () => {
|
||||
mockPathname = '/snippets/snippet-1/orchestrate'
|
||||
snippetDraftState.inputFields = snippetFields
|
||||
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
|
||||
snippetNavigationState.snippet = snippet
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('snippet-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('snippet-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
@ -704,13 +704,13 @@ describe('MainNav', () => {
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
})
|
||||
|
||||
it('replaces global navigation with app detail navigation on app routes', () => {
|
||||
it('replaces global navigation with app detail navigation on app routes', async () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByTestId('app-detail-top')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-detail-section')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('app-detail-top')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('app-detail-section')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-62')
|
||||
@ -742,56 +742,56 @@ describe('MainNav', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses app detail navigation from the top-right toggle', () => {
|
||||
it('collapses app detail navigation from the top-right toggle', async () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('app-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).not.toHaveClass('transition-none')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'false')
|
||||
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
})
|
||||
|
||||
it('shows app detail navigation as a floating preview when hovering the collapsed top toggle', () => {
|
||||
it('shows app detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('app-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('app-detail-top').parentElement!)
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
expect(screen.getAllByTestId('app-detail-top')).toHaveLength(1)
|
||||
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it('persists expanded app detail navigation without width animation when clicking the hovered toggle', () => {
|
||||
it('persists expanded app detail navigation without width animation when clicking the hovered toggle', async () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('app-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('app-detail-top').parentElement!)
|
||||
fireEvent.click(screen.getByTestId('app-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-62', 'transition-none')
|
||||
expect(screen.getByRole('complementary')).not.toHaveClass('overflow-visible')
|
||||
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('expand')
|
||||
})
|
||||
|
||||
it('replaces global navigation with dataset detail navigation on dataset routes', () => {
|
||||
it('replaces global navigation with dataset detail navigation on dataset routes', async () => {
|
||||
mockPathname = '/datasets/dataset-1/documents'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByTestId('dataset-detail-top')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('dataset-detail-section')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('dataset-detail-top')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('dataset-detail-section')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-62')
|
||||
@ -802,42 +802,44 @@ describe('MainNav', () => {
|
||||
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses dataset detail navigation from the top-right toggle', () => {
|
||||
it('collapses dataset detail navigation from the top-right toggle', async () => {
|
||||
mockPathname = '/datasets/dataset-1/documents'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('dataset-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('dataset-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'false')
|
||||
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(await screen.findByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
})
|
||||
|
||||
it('shows dataset detail navigation as a floating preview when hovering the collapsed top toggle', () => {
|
||||
it('shows dataset detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
|
||||
mockPathname = '/datasets/dataset-1/documents'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('dataset-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('dataset-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('dataset-detail-top').parentElement!)
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
expect(screen.getAllByTestId('dataset-detail-top')).toHaveLength(1)
|
||||
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(await screen.findByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
|
||||
it('replaces global navigation with agent detail navigation on roster detail routes', async () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('agent-detail-top')).toBeInTheDocument()
|
||||
const agentDetailNavigation = await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })
|
||||
expect(agentDetailNavigation).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /agentV2.agentDetail.sections.configure/ })).toHaveAttribute('href', '/roster/agent/agent-1/configure')
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(agentDetailNavigation).toHaveClass('px-1')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-62')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
|
||||
@ -873,16 +875,16 @@ describe('MainNav', () => {
|
||||
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses agent detail navigation from the top-right toggle', () => {
|
||||
it('collapses agent detail navigation from the top-right toggle', async () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('agent-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })).toHaveClass('px-3')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
})
|
||||
|
||||
@ -923,18 +925,18 @@ describe('MainNav', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
|
||||
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
fireEvent.click(await screen.findByTestId('agent-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
|
||||
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
|
||||
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })).toHaveClass('px-1')
|
||||
})
|
||||
|
||||
it.each([
|
||||
@ -1258,12 +1260,12 @@ describe('MainNav', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('collapses and expands installed web apps from the section arrow', () => {
|
||||
it('collapses and expands installed web apps from the section arrow', async () => {
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
|
||||
renderMainNav()
|
||||
|
||||
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
|
||||
const webAppsButton = await screen.findByRole('button', { name: 'explore.sidebar.webApps' })
|
||||
expect(webAppsButton).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('Alpha App')).toBeInTheDocument()
|
||||
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { SnippetCollapsedPreview } from '@/app/components/snippets/components/snippet-collapsed-preview'
|
||||
import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
|
||||
import { useSnippetDraftStore } from '@/app/components/snippets/draft-store'
|
||||
import { useSnippetDetailStore } from '@/app/components/snippets/store'
|
||||
|
||||
type SnippetDetailSectionProps = {
|
||||
expand: boolean
|
||||
}
|
||||
|
||||
export function SnippetDetailSection({ expand }: SnippetDetailSectionProps) {
|
||||
const snippetNavigation = useSnippetDetailStore(useShallow(state => ({
|
||||
onFieldsChange: state.onFieldsChange,
|
||||
readonly: state.readonly,
|
||||
snippet: state.snippet,
|
||||
})))
|
||||
const snippetInputFields = useSnippetDraftStore(state => state.inputFields)
|
||||
|
||||
if (!expand)
|
||||
return <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} />
|
||||
|
||||
if (!snippetNavigation.snippet || !snippetNavigation.onFieldsChange)
|
||||
return null
|
||||
|
||||
return (
|
||||
<SnippetSidebarContent
|
||||
snippet={snippetNavigation.snippet}
|
||||
fields={snippetInputFields}
|
||||
readonly={snippetNavigation.readonly}
|
||||
onFieldsChange={snippetNavigation.onFieldsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -7,31 +7,21 @@ import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import AppDetailSection from '@/app/components/app-sidebar/app-detail-section'
|
||||
import AppDetailTop from '@/app/components/app-sidebar/app-detail-top'
|
||||
import DatasetDetailSection from '@/app/components/app-sidebar/dataset-detail-section'
|
||||
import DatasetDetailTop from '@/app/components/app-sidebar/dataset-detail-top'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import EnvNav from '@/app/components/header/env-nav'
|
||||
import { SnippetCollapsedPreview } from '@/app/components/snippets/components/snippet-collapsed-preview'
|
||||
import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
|
||||
import { useSnippetDraftStore } from '@/app/components/snippets/draft-store'
|
||||
import { useSnippetDetailStore } from '@/app/components/snippets/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
|
||||
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
|
||||
import { DeploymentDetailSection, DeploymentDetailTop } from '@/features/deployments/detail/deployment-sidebar'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import Link from '@/next/link'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
import AccountSection from './components/account-section'
|
||||
import HelpMenu from './components/help-menu'
|
||||
import MainNavLink from './components/nav-link'
|
||||
import { MainNavSearchButton } from './components/search-button'
|
||||
import SnippetDetailTop from './components/snippet-detail-top'
|
||||
import WebAppsSection from './components/web-apps-section'
|
||||
import { WorkspaceCard } from './components/workspace-card'
|
||||
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
|
||||
import { useDetailSidebarMode } from './storage'
|
||||
@ -41,6 +31,16 @@ const DATASET_DOCUMENT_CREATION_ROUTES = new Set(['create', 'create-from-pipelin
|
||||
const DEPLOYMENT_COLLECTION_ROUTES = new Set(['create'])
|
||||
const secondarySidebarHelpTriggerIcon = <span aria-hidden className="i-ri-question-line size-4 shrink-0" />
|
||||
|
||||
const AppDetailSection = dynamic(() => import('@/app/components/app-sidebar/app-detail-section'), { ssr: false })
|
||||
const AppDetailTop = dynamic(() => import('@/app/components/app-sidebar/app-detail-top'), { ssr: false })
|
||||
const DatasetDetailSection = dynamic(() => import('@/app/components/app-sidebar/dataset-detail-section'), { ssr: false })
|
||||
const DatasetDetailTop = dynamic(() => import('@/app/components/app-sidebar/dataset-detail-top'), { ssr: false })
|
||||
const AgentDetailSection = dynamic(() => import('@/features/agent-v2/agent-detail/navigation').then(mod => mod.AgentDetailSection), { ssr: false })
|
||||
const AgentDetailTop = dynamic(() => import('@/features/agent-v2/agent-detail/navigation').then(mod => mod.AgentDetailTop), { ssr: false })
|
||||
const SnippetDetailTop = dynamic(() => import('./components/snippet-detail-top'), { ssr: false })
|
||||
const SnippetDetailSection = dynamic(() => import('./components/snippet-detail-section').then(mod => mod.SnippetDetailSection), { ssr: false })
|
||||
const WebAppsSection = dynamic(() => import('./components/web-apps-section'), { ssr: false })
|
||||
|
||||
function SecondarySidebarHelpMenu({
|
||||
triggerClassName,
|
||||
}: {
|
||||
@ -103,12 +103,6 @@ export function MainNav({
|
||||
const showDeploymentDetailNavigation = canUseAppDeploy && !isCurrentWorkspaceDatasetOperator && isDeploymentDetailPathname(pathname)
|
||||
const showSnippetDetailNavigation = isSnippetDetailPathname(pathname)
|
||||
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation || showSnippetDetailNavigation
|
||||
const snippetNavigation = useSnippetDetailStore(useShallow(state => ({
|
||||
onFieldsChange: state.onFieldsChange,
|
||||
readonly: state.readonly,
|
||||
snippet: state.snippet,
|
||||
})))
|
||||
const snippetInputFields = useSnippetDraftStore(state => state.inputFields)
|
||||
const { hasAppDetail, setAppDetail } = useAppStore(useShallow(state => ({
|
||||
hasAppDetail: !!state.appDetail,
|
||||
setAppDetail: state.setAppDetail,
|
||||
@ -307,18 +301,7 @@ export function MainNav({
|
||||
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: showDeploymentDetailNavigation
|
||||
? <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: detailNavigationVisibleExpanded
|
||||
? snippetNavigation.snippet && snippetNavigation.onFieldsChange
|
||||
? (
|
||||
<SnippetSidebarContent
|
||||
snippet={snippetNavigation.snippet}
|
||||
fields={snippetInputFields}
|
||||
readonly={snippetNavigation.readonly}
|
||||
onFieldsChange={snippetNavigation.onFieldsChange}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
: <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} />
|
||||
: <SnippetDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: (
|
||||
<>
|
||||
<nav className="isolate flex flex-col gap-px p-2">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PluginsSearchParams } from './types'
|
||||
import type { MarketPlaceInputs } from '@/contract/router'
|
||||
import type { MarketPlaceInputs } from '@/contract/marketplace'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { marketplaceQuery } from '@/service/client'
|
||||
import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins } from './utils'
|
||||
|
||||
13
web/contract/console/agent.ts
Normal file
13
web/contract/console/agent.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { agent } from '@dify/contracts/api/console/agent/orpc.gen'
|
||||
import { agentDriveContracts } from './agent-drive'
|
||||
|
||||
export const agentRouterContract = {
|
||||
...agent,
|
||||
byAgentId: {
|
||||
...agent.byAgentId,
|
||||
drive: {
|
||||
...agent.byAgentId.drive,
|
||||
...agentDriveContracts.byAgentId.drive,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
import type { AppListResponse, WorkflowOnlineUsersResponse } from '@/models/app'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { apps } from '@dify/contracts/api/console/apps/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
import { agentDriveContracts } from './agent-drive'
|
||||
|
||||
export type AppListSortBy = 'last_modified' | 'recently_created' | 'earliest_created'
|
||||
type AppListMode = AppModeEnum | 'agent' | 'channel' | 'all'
|
||||
@ -85,3 +87,24 @@ export const workflowOnlineUsersContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowOnlineUsersResponse>())
|
||||
|
||||
export const appsRouterContract = {
|
||||
...apps,
|
||||
list: appListContract,
|
||||
deleteApp: appDeleteContract,
|
||||
starredList: appStarredListContract,
|
||||
star: appStarContract,
|
||||
unstar: appUnstarContract,
|
||||
workflowOnlineUsers: workflowOnlineUsersContract,
|
||||
byAppId: {
|
||||
...apps.byAppId,
|
||||
agent: {
|
||||
...apps.byAppId.agent,
|
||||
...agentDriveContracts.byAppId.agent,
|
||||
drive: {
|
||||
...apps.byAppId.agent.drive,
|
||||
...agentDriveContracts.byAppId.agent.drive,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { billing } from '@dify/contracts/api/console/billing/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -23,3 +24,9 @@ export const bindPartnerStackContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const billingRouterContract = {
|
||||
...billing,
|
||||
invoices: invoicesContract,
|
||||
bindPartnerStack: bindPartnerStackContract,
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Banner } from '@/models/app'
|
||||
import type { App, AppCategory, InstalledApp } from '@/models/explore'
|
||||
import type { AppMeta } from '@/models/share'
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { explore } from '@dify/contracts/api/console/explore/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -142,3 +143,18 @@ export const exploreBannersContract = base
|
||||
})
|
||||
.input(type<{ query?: { language?: string } }>())
|
||||
.output(type<Banner[]>())
|
||||
|
||||
export const exploreRouterContract = {
|
||||
...explore,
|
||||
apps: exploreAppsContract,
|
||||
learnDifyApps: learnDifyAppsContract,
|
||||
appDetail: exploreAppDetailContract,
|
||||
installedApps: exploreInstalledAppsContract,
|
||||
uninstallInstalledApp: exploreInstalledAppUninstallContract,
|
||||
updateInstalledApp: exploreInstalledAppPinContract,
|
||||
appAccessMode: exploreInstalledAppAccessModeContract,
|
||||
updateAppAccessMode: exploreInstalledAppAccessModeUpdateContract,
|
||||
installedAppParameters: exploreInstalledAppParametersContract,
|
||||
installedAppMeta: exploreInstalledAppMetaContract,
|
||||
banners: exploreBannersContract,
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { PostFilesUploadResponse } from '@dify/contracts/api/console/files/types.gen'
|
||||
import { files } from '@dify/contracts/api/console/files/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -14,3 +15,11 @@ export const fileUploadContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<PostFilesUploadResponse>())
|
||||
|
||||
export const filesRouterContract = {
|
||||
...files,
|
||||
upload: {
|
||||
...files.upload,
|
||||
post: fileUploadContract,
|
||||
},
|
||||
}
|
||||
|
||||
@ -31,3 +31,8 @@ export const changePreferredProviderTypeContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<CommonResponse>())
|
||||
|
||||
export const modelProvidersRouterContract = {
|
||||
models: modelProvidersModelsContract,
|
||||
changePreferredProviderType: changePreferredProviderTypeContract,
|
||||
}
|
||||
|
||||
@ -25,3 +25,8 @@ export const pluginLatestVersionsContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<InstalledLatestVersionResponse>())
|
||||
|
||||
export const pluginsRouterContract = {
|
||||
checkInstalled: pluginCheckInstalledContract,
|
||||
latestVersions: pluginLatestVersionsContract,
|
||||
}
|
||||
|
||||
@ -342,3 +342,31 @@ export const stopSnippetWorkflowTaskContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const snippetsRouterContract = {
|
||||
list: listCustomizedSnippetsContract,
|
||||
create: createCustomizedSnippetContract,
|
||||
detail: getCustomizedSnippetContract,
|
||||
update: updateCustomizedSnippetContract,
|
||||
delete: deleteCustomizedSnippetContract,
|
||||
export: exportCustomizedSnippetContract,
|
||||
import: importCustomizedSnippetContract,
|
||||
confirmImport: confirmSnippetImportContract,
|
||||
checkDependencies: checkSnippetDependenciesContract,
|
||||
incrementUseCount: incrementSnippetUseCountContract,
|
||||
draftWorkflow: getSnippetDraftWorkflowContract,
|
||||
syncDraftWorkflow: syncSnippetDraftWorkflowContract,
|
||||
draftConfig: getSnippetDraftConfigContract,
|
||||
publishedWorkflow: getSnippetPublishedWorkflowContract,
|
||||
publishWorkflow: publishSnippetWorkflowContract,
|
||||
defaultBlockConfigs: getSnippetDefaultBlockConfigsContract,
|
||||
workflowRuns: listSnippetWorkflowRunsContract,
|
||||
workflowRunDetail: getSnippetWorkflowRunDetailContract,
|
||||
workflowRunNodeExecutions: listSnippetWorkflowRunNodeExecutionsContract,
|
||||
runDraftNode: runSnippetDraftNodeContract,
|
||||
lastDraftNodeRun: getSnippetDraftNodeLastRunContract,
|
||||
runDraftIterationNode: runSnippetDraftIterationNodeContract,
|
||||
runDraftLoopNode: runSnippetDraftLoopNodeContract,
|
||||
runDraftWorkflow: runSnippetDraftWorkflowContract,
|
||||
stopWorkflowTask: stopSnippetWorkflowTaskContract,
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { tags } from '@dify/contracts/api/console/tags/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -89,3 +90,13 @@ export const tagBindingRemoveContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const tagsRouterContract = {
|
||||
...tags,
|
||||
list: tagListContract,
|
||||
create: tagCreateContract,
|
||||
update: tagUpdateContract,
|
||||
delete: tagDeleteContract,
|
||||
bind: tagBindingCreateContract,
|
||||
unbind: tagBindingRemoveContract,
|
||||
}
|
||||
|
||||
@ -117,3 +117,21 @@ export const triggerOAuthInitiateContract = base
|
||||
.route({ path: '/workspaces/current/trigger-provider/{provider}/subscriptions/oauth/authorize', method: 'GET' })
|
||||
.input(type<{ params: { provider: string } }>())
|
||||
.output(type<{ authorization_url: string, subscription_builder: TriggerSubscriptionBuilder }>())
|
||||
|
||||
export const triggersRouterContract = {
|
||||
list: triggersContract,
|
||||
providerInfo: triggerProviderInfoContract,
|
||||
subscriptions: triggerSubscriptionsContract,
|
||||
subscriptionBuilderCreate: triggerSubscriptionBuilderCreateContract,
|
||||
subscriptionBuilderUpdate: triggerSubscriptionBuilderUpdateContract,
|
||||
subscriptionBuilderVerifyUpdate: triggerSubscriptionBuilderVerifyUpdateContract,
|
||||
subscriptionVerify: triggerSubscriptionVerifyContract,
|
||||
subscriptionBuild: triggerSubscriptionBuildContract,
|
||||
subscriptionDelete: triggerSubscriptionDeleteContract,
|
||||
subscriptionUpdate: triggerSubscriptionUpdateContract,
|
||||
subscriptionBuilderLogs: triggerSubscriptionBuilderLogsContract,
|
||||
oauthConfig: triggerOAuthConfigContract,
|
||||
oauthConfigure: triggerOAuthConfigureContract,
|
||||
oauthDelete: triggerOAuthDeleteContract,
|
||||
oauthInitiate: triggerOAuthInitiateContract,
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ChatConfig } from '@/app/components/base/chat/types'
|
||||
import type { DataSetListResponse } from '@/models/datasets'
|
||||
import type { TryAppFlowPreview, TryAppInfo } from '@/models/try-app'
|
||||
import { trialApps } from '@dify/contracts/api/console/trial-apps/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -54,3 +55,11 @@ export const trialAppParametersContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<ChatConfig>())
|
||||
|
||||
export const trialAppsRouterContract = {
|
||||
...trialApps,
|
||||
info: trialAppInfoContract,
|
||||
datasets: trialAppDatasetsContract,
|
||||
parameters: trialAppParametersContract,
|
||||
workflows: trialAppWorkflowsContract,
|
||||
}
|
||||
|
||||
@ -78,3 +78,10 @@ export const workflowDraftUpdateFeaturesContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<CommonResponse>())
|
||||
|
||||
export const workflowDraftRouterContract = {
|
||||
environmentVariables: workflowDraftEnvironmentVariablesContract,
|
||||
updateEnvironmentVariables: workflowDraftUpdateEnvironmentVariablesContract,
|
||||
updateConversationVariables: workflowDraftUpdateConversationVariablesContract,
|
||||
updateFeatures: workflowDraftUpdateFeaturesContract,
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
@ -54,3 +55,11 @@ export const workspaceSwitchContract = base
|
||||
body: SwitchWorkspaceRequest
|
||||
}>())
|
||||
.output(type<SwitchWorkspaceResponse>())
|
||||
|
||||
export const workspacesRouterContract = {
|
||||
...workspaces,
|
||||
get: workspacesGetContract,
|
||||
switch: {
|
||||
post: workspaceSwitchContract,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, PluginsSearchParams } from '@/app/components/plugins/marketplace/types'
|
||||
import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
|
||||
import type { MarketplaceTemplate } from '@/types/marketplace-template'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from './base'
|
||||
|
||||
export const collectionsContract = base
|
||||
const collectionsContract = base
|
||||
.route({
|
||||
path: '/collections',
|
||||
method: 'GET',
|
||||
@ -22,7 +23,7 @@ export const collectionsContract = base
|
||||
}>(),
|
||||
)
|
||||
|
||||
export const collectionPluginsContract = base
|
||||
const collectionPluginsContract = base
|
||||
.route({
|
||||
path: '/collections/{collectionId}/plugins',
|
||||
method: 'POST',
|
||||
@ -43,7 +44,7 @@ export const collectionPluginsContract = base
|
||||
}>(),
|
||||
)
|
||||
|
||||
export const searchAdvancedContract = base
|
||||
const searchAdvancedContract = base
|
||||
.route({
|
||||
path: '/{kind}/search/advanced',
|
||||
method: 'POST',
|
||||
@ -56,7 +57,7 @@ export const searchAdvancedContract = base
|
||||
}>())
|
||||
.output(type<{ data: PluginsFromMarketplaceResponse }>())
|
||||
|
||||
export const templateDetailContract = base
|
||||
const templateDetailContract = base
|
||||
.route({
|
||||
path: '/templates/{templateId}',
|
||||
method: 'GET',
|
||||
@ -68,7 +69,7 @@ export const templateDetailContract = base
|
||||
}>())
|
||||
.output(type<{ data: MarketplaceTemplate }>())
|
||||
|
||||
export const downloadPluginContract = base
|
||||
const downloadPluginContract = base
|
||||
.route({
|
||||
path: '/plugins/{organization}/{pluginName}/{version}/download',
|
||||
method: 'GET',
|
||||
@ -81,3 +82,13 @@ export const downloadPluginContract = base
|
||||
}
|
||||
}>())
|
||||
.output(type<Blob>())
|
||||
|
||||
export const marketplaceRouterContract = {
|
||||
collections: collectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
templateDetail: templateDetailContract,
|
||||
downloadPlugin: downloadPluginContract,
|
||||
}
|
||||
|
||||
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>
|
||||
|
||||
@ -1,249 +1,120 @@
|
||||
import type { InferContractRouterInputs } from '@orpc/contract'
|
||||
import { contract as communityContract } from '@dify/contracts/api/console/orpc.gen'
|
||||
import { account } from '@dify/contracts/api/console/account/orpc.gen'
|
||||
import { activate } from '@dify/contracts/api/console/activate/orpc.gen'
|
||||
import { allWorkspaces } from '@dify/contracts/api/console/all-workspaces/orpc.gen'
|
||||
import { apiBasedExtension } from '@dify/contracts/api/console/api-based-extension/orpc.gen'
|
||||
import { apiKeyAuth } from '@dify/contracts/api/console/api-key-auth/orpc.gen'
|
||||
import { appDslVersion } from '@dify/contracts/api/console/app-dsl-version/orpc.gen'
|
||||
import { app } from '@dify/contracts/api/console/app/orpc.gen'
|
||||
import { auth } from '@dify/contracts/api/console/auth/orpc.gen'
|
||||
import { codeBasedExtension } from '@dify/contracts/api/console/code-based-extension/orpc.gen'
|
||||
import { compliance } from '@dify/contracts/api/console/compliance/orpc.gen'
|
||||
import { dataSource } from '@dify/contracts/api/console/data-source/orpc.gen'
|
||||
import { datasets } from '@dify/contracts/api/console/datasets/orpc.gen'
|
||||
import { emailCodeLogin } from '@dify/contracts/api/console/email-code-login/orpc.gen'
|
||||
import { emailRegister } from '@dify/contracts/api/console/email-register/orpc.gen'
|
||||
import { features } from '@dify/contracts/api/console/features/orpc.gen'
|
||||
import { forgotPassword } from '@dify/contracts/api/console/forgot-password/orpc.gen'
|
||||
import { form } from '@dify/contracts/api/console/form/orpc.gen'
|
||||
import { info } from '@dify/contracts/api/console/info/orpc.gen'
|
||||
import { installedApps } from '@dify/contracts/api/console/installed-apps/orpc.gen'
|
||||
import { instructionGenerate } from '@dify/contracts/api/console/instruction-generate/orpc.gen'
|
||||
import { login } from '@dify/contracts/api/console/login/orpc.gen'
|
||||
import { logout } from '@dify/contracts/api/console/logout/orpc.gen'
|
||||
import { notion } from '@dify/contracts/api/console/notion/orpc.gen'
|
||||
import { oauth } from '@dify/contracts/api/console/oauth/orpc.gen'
|
||||
import { rag } from '@dify/contracts/api/console/rag/orpc.gen'
|
||||
import { refreshToken } from '@dify/contracts/api/console/refresh-token/orpc.gen'
|
||||
import { remoteFiles } from '@dify/contracts/api/console/remote-files/orpc.gen'
|
||||
import { resetPassword } from '@dify/contracts/api/console/reset-password/orpc.gen'
|
||||
import { ruleCodeGenerate } from '@dify/contracts/api/console/rule-code-generate/orpc.gen'
|
||||
import { ruleGenerate } from '@dify/contracts/api/console/rule-generate/orpc.gen'
|
||||
import { ruleStructuredOutputGenerate } from '@dify/contracts/api/console/rule-structured-output-generate/orpc.gen'
|
||||
import { spec } from '@dify/contracts/api/console/spec/orpc.gen'
|
||||
import { systemFeatures } from '@dify/contracts/api/console/system-features/orpc.gen'
|
||||
import { tagBindings } from '@dify/contracts/api/console/tag-bindings/orpc.gen'
|
||||
import { test } from '@dify/contracts/api/console/test/orpc.gen'
|
||||
import { trialModels } from '@dify/contracts/api/console/trial-models/orpc.gen'
|
||||
import { website } from '@dify/contracts/api/console/website/orpc.gen'
|
||||
import { workflowGenerate } from '@dify/contracts/api/console/workflow-generate/orpc.gen'
|
||||
import { workflow } from '@dify/contracts/api/console/workflow/orpc.gen'
|
||||
import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen'
|
||||
import { rbacAccessConfigContract } from './console/access-control'
|
||||
import { agentDriveContracts } from './console/agent-drive'
|
||||
import {
|
||||
appDeleteContract,
|
||||
appListContract,
|
||||
appStarContract,
|
||||
appStarredListContract,
|
||||
appUnstarContract,
|
||||
workflowOnlineUsersContract,
|
||||
} from './console/apps'
|
||||
import { bindPartnerStackContract, invoicesContract } from './console/billing'
|
||||
import {
|
||||
exploreAppDetailContract,
|
||||
exploreAppsContract,
|
||||
exploreBannersContract,
|
||||
exploreInstalledAppAccessModeContract,
|
||||
exploreInstalledAppAccessModeUpdateContract,
|
||||
exploreInstalledAppMetaContract,
|
||||
exploreInstalledAppParametersContract,
|
||||
exploreInstalledAppPinContract,
|
||||
exploreInstalledAppsContract,
|
||||
exploreInstalledAppUninstallContract,
|
||||
learnDifyAppsContract,
|
||||
} from './console/explore'
|
||||
import { fileUploadContract } from './console/files'
|
||||
import { changePreferredProviderTypeContract, modelProvidersModelsContract } from './console/model-providers'
|
||||
import { agentRouterContract } from './console/agent'
|
||||
import { appsRouterContract } from './console/apps'
|
||||
import { billingRouterContract } from './console/billing'
|
||||
import { exploreRouterContract } from './console/explore'
|
||||
import { filesRouterContract } from './console/files'
|
||||
import { modelProvidersRouterContract } from './console/model-providers'
|
||||
import { notificationContract, notificationDismissContract } from './console/notification'
|
||||
import { pluginCheckInstalledContract, pluginLatestVersionsContract } from './console/plugins'
|
||||
import {
|
||||
checkSnippetDependenciesContract,
|
||||
confirmSnippetImportContract,
|
||||
createCustomizedSnippetContract,
|
||||
deleteCustomizedSnippetContract,
|
||||
exportCustomizedSnippetContract,
|
||||
getCustomizedSnippetContract,
|
||||
getSnippetDefaultBlockConfigsContract,
|
||||
getSnippetDraftConfigContract,
|
||||
getSnippetDraftNodeLastRunContract,
|
||||
getSnippetDraftWorkflowContract,
|
||||
getSnippetPublishedWorkflowContract,
|
||||
getSnippetWorkflowRunDetailContract,
|
||||
importCustomizedSnippetContract,
|
||||
incrementSnippetUseCountContract,
|
||||
listCustomizedSnippetsContract,
|
||||
listSnippetWorkflowRunNodeExecutionsContract,
|
||||
listSnippetWorkflowRunsContract,
|
||||
publishSnippetWorkflowContract,
|
||||
runSnippetDraftIterationNodeContract,
|
||||
runSnippetDraftLoopNodeContract,
|
||||
runSnippetDraftNodeContract,
|
||||
runSnippetDraftWorkflowContract,
|
||||
stopSnippetWorkflowTaskContract,
|
||||
syncSnippetDraftWorkflowContract,
|
||||
updateCustomizedSnippetContract,
|
||||
} from './console/snippets'
|
||||
import {
|
||||
tagBindingCreateContract,
|
||||
tagBindingRemoveContract,
|
||||
tagCreateContract,
|
||||
tagDeleteContract,
|
||||
tagListContract,
|
||||
tagUpdateContract,
|
||||
} from './console/tags'
|
||||
import {
|
||||
triggerOAuthConfigContract,
|
||||
triggerOAuthConfigureContract,
|
||||
triggerOAuthDeleteContract,
|
||||
triggerOAuthInitiateContract,
|
||||
triggerProviderInfoContract,
|
||||
triggersContract,
|
||||
triggerSubscriptionBuildContract,
|
||||
triggerSubscriptionBuilderCreateContract,
|
||||
triggerSubscriptionBuilderLogsContract,
|
||||
triggerSubscriptionBuilderUpdateContract,
|
||||
triggerSubscriptionBuilderVerifyUpdateContract,
|
||||
triggerSubscriptionDeleteContract,
|
||||
triggerSubscriptionsContract,
|
||||
triggerSubscriptionUpdateContract,
|
||||
triggerSubscriptionVerifyContract,
|
||||
} from './console/trigger'
|
||||
import { trialAppDatasetsContract, trialAppInfoContract, trialAppParametersContract, trialAppWorkflowsContract } from './console/try-app'
|
||||
import {
|
||||
workflowDraftEnvironmentVariablesContract,
|
||||
workflowDraftUpdateConversationVariablesContract,
|
||||
workflowDraftUpdateEnvironmentVariablesContract,
|
||||
workflowDraftUpdateFeaturesContract,
|
||||
} from './console/workflow'
|
||||
import { pluginsRouterContract } from './console/plugins'
|
||||
import { snippetsRouterContract } from './console/snippets'
|
||||
import { tagsRouterContract } from './console/tags'
|
||||
import { triggersRouterContract } from './console/trigger'
|
||||
import { trialAppsRouterContract } from './console/try-app'
|
||||
import { workflowDraftRouterContract } from './console/workflow'
|
||||
import { workflowCommentContracts } from './console/workflow-comment'
|
||||
import { workspacesGetContract, workspaceSwitchContract } from './console/workspaces'
|
||||
import { collectionPluginsContract, collectionsContract, downloadPluginContract, searchAdvancedContract, templateDetailContract } from './marketplace'
|
||||
import { workspacesRouterContract } from './console/workspaces'
|
||||
|
||||
export const marketplaceRouterContract = {
|
||||
collections: collectionsContract,
|
||||
collectionPlugins: collectionPluginsContract,
|
||||
searchAdvanced: searchAdvancedContract,
|
||||
templateDetail: templateDetailContract,
|
||||
downloadPlugin: downloadPluginContract,
|
||||
const communityContract = {
|
||||
account,
|
||||
activate,
|
||||
allWorkspaces,
|
||||
apiBasedExtension,
|
||||
apiKeyAuth,
|
||||
app,
|
||||
appDslVersion,
|
||||
auth,
|
||||
codeBasedExtension,
|
||||
compliance,
|
||||
dataSource,
|
||||
datasets,
|
||||
emailCodeLogin,
|
||||
emailRegister,
|
||||
features,
|
||||
forgotPassword,
|
||||
form,
|
||||
info,
|
||||
installedApps,
|
||||
instructionGenerate,
|
||||
login,
|
||||
logout,
|
||||
notion,
|
||||
oauth,
|
||||
rag,
|
||||
refreshToken,
|
||||
remoteFiles,
|
||||
resetPassword,
|
||||
ruleCodeGenerate,
|
||||
ruleGenerate,
|
||||
ruleStructuredOutputGenerate,
|
||||
spec,
|
||||
systemFeatures,
|
||||
tagBindings,
|
||||
test,
|
||||
trialModels,
|
||||
website,
|
||||
workflow,
|
||||
workflowGenerate,
|
||||
}
|
||||
|
||||
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>
|
||||
|
||||
export const consoleRouterContract = {
|
||||
enterprise: enterpriseContract,
|
||||
...communityContract,
|
||||
apps: {
|
||||
...communityContract.apps,
|
||||
list: appListContract,
|
||||
deleteApp: appDeleteContract,
|
||||
starredList: appStarredListContract,
|
||||
star: appStarContract,
|
||||
unstar: appUnstarContract,
|
||||
workflowOnlineUsers: workflowOnlineUsersContract,
|
||||
byAppId: {
|
||||
...communityContract.apps.byAppId,
|
||||
agent: {
|
||||
...communityContract.apps.byAppId.agent,
|
||||
...agentDriveContracts.byAppId.agent,
|
||||
drive: {
|
||||
...communityContract.apps.byAppId.agent.drive,
|
||||
...agentDriveContracts.byAppId.agent.drive,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
...communityContract.agent,
|
||||
byAgentId: {
|
||||
...communityContract.agent.byAgentId,
|
||||
drive: {
|
||||
...communityContract.agent.byAgentId.drive,
|
||||
...agentDriveContracts.byAgentId.drive,
|
||||
},
|
||||
},
|
||||
},
|
||||
explore: {
|
||||
...communityContract.explore,
|
||||
apps: exploreAppsContract,
|
||||
learnDifyApps: learnDifyAppsContract,
|
||||
appDetail: exploreAppDetailContract,
|
||||
installedApps: exploreInstalledAppsContract,
|
||||
uninstallInstalledApp: exploreInstalledAppUninstallContract,
|
||||
updateInstalledApp: exploreInstalledAppPinContract,
|
||||
appAccessMode: exploreInstalledAppAccessModeContract,
|
||||
updateAppAccessMode: exploreInstalledAppAccessModeUpdateContract,
|
||||
installedAppParameters: exploreInstalledAppParametersContract,
|
||||
installedAppMeta: exploreInstalledAppMetaContract,
|
||||
banners: exploreBannersContract,
|
||||
},
|
||||
trialApps: {
|
||||
...communityContract.trialApps,
|
||||
info: trialAppInfoContract,
|
||||
datasets: trialAppDatasetsContract,
|
||||
parameters: trialAppParametersContract,
|
||||
workflows: trialAppWorkflowsContract,
|
||||
},
|
||||
files: {
|
||||
...communityContract.files,
|
||||
upload: {
|
||||
...communityContract.files.upload,
|
||||
post: fileUploadContract,
|
||||
},
|
||||
},
|
||||
modelProviders: {
|
||||
models: modelProvidersModelsContract,
|
||||
changePreferredProviderType: changePreferredProviderTypeContract,
|
||||
},
|
||||
plugins: {
|
||||
checkInstalled: pluginCheckInstalledContract,
|
||||
latestVersions: pluginLatestVersionsContract,
|
||||
},
|
||||
rbacAccessConfig: rbacAccessConfigContract,
|
||||
snippets: {
|
||||
list: listCustomizedSnippetsContract,
|
||||
create: createCustomizedSnippetContract,
|
||||
detail: getCustomizedSnippetContract,
|
||||
update: updateCustomizedSnippetContract,
|
||||
delete: deleteCustomizedSnippetContract,
|
||||
export: exportCustomizedSnippetContract,
|
||||
import: importCustomizedSnippetContract,
|
||||
confirmImport: confirmSnippetImportContract,
|
||||
checkDependencies: checkSnippetDependenciesContract,
|
||||
incrementUseCount: incrementSnippetUseCountContract,
|
||||
draftWorkflow: getSnippetDraftWorkflowContract,
|
||||
syncDraftWorkflow: syncSnippetDraftWorkflowContract,
|
||||
draftConfig: getSnippetDraftConfigContract,
|
||||
publishedWorkflow: getSnippetPublishedWorkflowContract,
|
||||
publishWorkflow: publishSnippetWorkflowContract,
|
||||
defaultBlockConfigs: getSnippetDefaultBlockConfigsContract,
|
||||
workflowRuns: listSnippetWorkflowRunsContract,
|
||||
workflowRunDetail: getSnippetWorkflowRunDetailContract,
|
||||
workflowRunNodeExecutions: listSnippetWorkflowRunNodeExecutionsContract,
|
||||
runDraftNode: runSnippetDraftNodeContract,
|
||||
lastDraftNodeRun: getSnippetDraftNodeLastRunContract,
|
||||
runDraftIterationNode: runSnippetDraftIterationNodeContract,
|
||||
runDraftLoopNode: runSnippetDraftLoopNodeContract,
|
||||
runDraftWorkflow: runSnippetDraftWorkflowContract,
|
||||
stopWorkflowTask: stopSnippetWorkflowTaskContract,
|
||||
},
|
||||
billing: {
|
||||
...communityContract.billing,
|
||||
invoices: invoicesContract,
|
||||
bindPartnerStack: bindPartnerStackContract,
|
||||
},
|
||||
workflowDraft: {
|
||||
environmentVariables: workflowDraftEnvironmentVariablesContract,
|
||||
updateEnvironmentVariables: workflowDraftUpdateEnvironmentVariablesContract,
|
||||
updateConversationVariables: workflowDraftUpdateConversationVariablesContract,
|
||||
updateFeatures: workflowDraftUpdateFeaturesContract,
|
||||
},
|
||||
workflowComments: workflowCommentContracts,
|
||||
agent: agentRouterContract,
|
||||
apps: appsRouterContract,
|
||||
billing: billingRouterContract,
|
||||
explore: exploreRouterContract,
|
||||
files: filesRouterContract,
|
||||
modelProviders: modelProvidersRouterContract,
|
||||
notification: notificationContract,
|
||||
notificationDismiss: notificationDismissContract,
|
||||
tags: {
|
||||
...communityContract.tags,
|
||||
list: tagListContract,
|
||||
create: tagCreateContract,
|
||||
update: tagUpdateContract,
|
||||
delete: tagDeleteContract,
|
||||
bind: tagBindingCreateContract,
|
||||
unbind: tagBindingRemoveContract,
|
||||
},
|
||||
triggers: {
|
||||
list: triggersContract,
|
||||
providerInfo: triggerProviderInfoContract,
|
||||
subscriptions: triggerSubscriptionsContract,
|
||||
subscriptionBuilderCreate: triggerSubscriptionBuilderCreateContract,
|
||||
subscriptionBuilderUpdate: triggerSubscriptionBuilderUpdateContract,
|
||||
subscriptionBuilderVerifyUpdate: triggerSubscriptionBuilderVerifyUpdateContract,
|
||||
subscriptionVerify: triggerSubscriptionVerifyContract,
|
||||
subscriptionBuild: triggerSubscriptionBuildContract,
|
||||
subscriptionDelete: triggerSubscriptionDeleteContract,
|
||||
subscriptionUpdate: triggerSubscriptionUpdateContract,
|
||||
subscriptionBuilderLogs: triggerSubscriptionBuilderLogsContract,
|
||||
oauthConfig: triggerOAuthConfigContract,
|
||||
oauthConfigure: triggerOAuthConfigureContract,
|
||||
oauthDelete: triggerOAuthDeleteContract,
|
||||
oauthInitiate: triggerOAuthInitiateContract,
|
||||
},
|
||||
workspaces: {
|
||||
...communityContract.workspaces,
|
||||
get: workspacesGetContract,
|
||||
switch: {
|
||||
post: workspaceSwitchContract,
|
||||
},
|
||||
},
|
||||
plugins: pluginsRouterContract,
|
||||
rbacAccessConfig: rbacAccessConfigContract,
|
||||
snippets: snippetsRouterContract,
|
||||
tags: tagsRouterContract,
|
||||
triggers: triggersRouterContract,
|
||||
trialApps: trialAppsRouterContract,
|
||||
workflowComments: workflowCommentContracts,
|
||||
workflowDraft: workflowDraftRouterContract,
|
||||
workspaces: workspacesRouterContract,
|
||||
}
|
||||
|
||||
@ -2,20 +2,21 @@
|
||||
|
||||
import type { ContractRouterClient } from '@orpc/contract'
|
||||
import type { JsonifiedClient } from '@orpc/openapi-client'
|
||||
import type { ConsoleRouterContract } from '@/service/console-link'
|
||||
import { createORPCClient } from '@orpc/client'
|
||||
import { OpenAPILink } from '@orpc/openapi-client/fetch'
|
||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { consoleRouterContract } from '@/contract/router'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { request } from '@/service/base'
|
||||
import { getBaseURL } from '@/service/client'
|
||||
import { createConsoleDynamicLink } from '@/service/console-link'
|
||||
|
||||
type AgentConfigureConsoleClientContext = {
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
const agentConfigureConsoleLink = new OpenAPILink<AgentConfigureConsoleClientContext>(consoleRouterContract, {
|
||||
const agentConfigureConsoleLink = createConsoleDynamicLink<AgentConfigureConsoleClientContext>(contract => new OpenAPILink<AgentConfigureConsoleClientContext>(contract, {
|
||||
url: getBaseURL(API_PREFIX),
|
||||
fetch: (input, init, options) => {
|
||||
return request(
|
||||
@ -28,9 +29,9 @@ const agentConfigureConsoleLink = new OpenAPILink<AgentConfigureConsoleClientCon
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
const agentConfigureConsoleClient: JsonifiedClient<ContractRouterClient<typeof consoleRouterContract, AgentConfigureConsoleClientContext>>
|
||||
const agentConfigureConsoleClient: JsonifiedClient<ContractRouterClient<ConsoleRouterContract, AgentConfigureConsoleClientContext>>
|
||||
= createORPCClient(agentConfigureConsoleLink)
|
||||
|
||||
export const agentConfigureConsoleQuery = createTanstackQueryUtils(agentConfigureConsoleClient, {
|
||||
|
||||
@ -26,6 +26,7 @@ type TabDef = {
|
||||
key: InstanceDetailTabKey
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
type TailwindNavIconProps = PropsWithoutRef<ComponentProps<'svg'>> & {
|
||||
@ -68,7 +69,7 @@ const DEPLOYMENT_TABS: TabDef[] = [
|
||||
{ key: 'overview', icon: OverviewIcon, selectedIcon: OverviewSelectedIcon },
|
||||
{ key: 'instances', icon: DeployIcon, selectedIcon: DeploySelectedIcon },
|
||||
{ key: 'releases', icon: VersionsIcon, selectedIcon: VersionsSelectedIcon },
|
||||
{ key: 'access', icon: AccessIcon, selectedIcon: AccessSelectedIcon },
|
||||
{ key: 'access', icon: AccessIcon, selectedIcon: AccessSelectedIcon, hidden: true },
|
||||
{ key: 'api-tokens', icon: ApiIcon, selectedIcon: ApiSelectedIcon },
|
||||
]
|
||||
|
||||
@ -296,7 +297,7 @@ export function DeploymentDetailSection({
|
||||
</div>
|
||||
|
||||
<nav className={cn('flex flex-col gap-y-0.5 py-1', expand ? 'px-1' : 'px-3')}>
|
||||
{DEPLOYMENT_TABS.map(tab => (
|
||||
{DEPLOYMENT_TABS.filter(tab => !tab.hidden).map(tab => (
|
||||
<NavLink
|
||||
key={tab.key}
|
||||
mode={expand ? 'expand' : 'collapse'}
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
import type { Resource } from 'i18next'
|
||||
import type { Locale } from '.'
|
||||
import type { Namespace, NamespaceInFileName } from './resources'
|
||||
import { kebabCase } from 'es-toolkit/string'
|
||||
import { createInstance } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
import { getI18n, initReactI18next } from 'react-i18next'
|
||||
import { loadI18nResource } from './load-resource'
|
||||
import { getInitOptions } from './settings'
|
||||
|
||||
export function createI18nextInstance(lng: Locale, resources: Resource) {
|
||||
@ -15,10 +15,7 @@ export function createI18nextInstance(lng: Locale, resources: Resource) {
|
||||
.use(resourcesToBackend((
|
||||
language: Locale,
|
||||
namespace: NamespaceInFileName | Namespace,
|
||||
) => {
|
||||
const namespaceKebab = kebabCase(namespace)
|
||||
return import(`../i18n/${language}/${namespaceKebab}.json`)
|
||||
}))
|
||||
) => loadI18nResource(language, namespace)))
|
||||
.init({
|
||||
...getInitOptions(),
|
||||
lng,
|
||||
|
||||
35
web/i18n-config/load-resource.ts
Normal file
35
web/i18n-config/load-resource.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { ResourceKey } from 'i18next'
|
||||
import type { Locale } from './language'
|
||||
import type { Namespace, NamespaceInFileName } from './resources'
|
||||
import { kebabCase } from 'es-toolkit/string'
|
||||
import { LanguagesSupported } from './language'
|
||||
|
||||
type LocaleResourceModule = {
|
||||
loadResource: (fileNamespace: string) => Promise<{ default: ResourceKey }>
|
||||
}
|
||||
|
||||
const legacyLocaleMap: Partial<Record<Locale, Locale>> = {
|
||||
en_US: 'en-US',
|
||||
ja_JP: 'ja-JP',
|
||||
zh_Hans: 'zh-Hans',
|
||||
}
|
||||
|
||||
const defaultLocale = 'en-US' satisfies Locale
|
||||
|
||||
const normalizeLocale = (locale: Locale): Locale => {
|
||||
const normalized = legacyLocaleMap[locale] ?? locale
|
||||
if (LanguagesSupported.includes(normalized))
|
||||
return normalized
|
||||
|
||||
return defaultLocale
|
||||
}
|
||||
|
||||
const loadLocaleResources = (locale: Locale): Promise<LocaleResourceModule> => {
|
||||
const normalized = normalizeLocale(locale)
|
||||
return import(`./locale-resources/${normalized}.ts`)
|
||||
}
|
||||
|
||||
export const loadI18nResource = async (locale: Locale, namespace: Namespace | NamespaceInFileName) => {
|
||||
const { loadResource } = await loadLocaleResources(locale)
|
||||
return loadResource(kebabCase(namespace))
|
||||
}
|
||||
1
web/i18n-config/locale-resources/ar-TN.ts
Normal file
1
web/i18n-config/locale-resources/ar-TN.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/ar-TN/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/de-DE.ts
Normal file
1
web/i18n-config/locale-resources/de-DE.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/de-DE/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/en-US.ts
Normal file
1
web/i18n-config/locale-resources/en-US.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/en-US/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/es-ES.ts
Normal file
1
web/i18n-config/locale-resources/es-ES.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/es-ES/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/fa-IR.ts
Normal file
1
web/i18n-config/locale-resources/fa-IR.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/fa-IR/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/fr-FR.ts
Normal file
1
web/i18n-config/locale-resources/fr-FR.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/fr-FR/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/hi-IN.ts
Normal file
1
web/i18n-config/locale-resources/hi-IN.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/hi-IN/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/id-ID.ts
Normal file
1
web/i18n-config/locale-resources/id-ID.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/id-ID/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/it-IT.ts
Normal file
1
web/i18n-config/locale-resources/it-IT.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/it-IT/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/ja-JP.ts
Normal file
1
web/i18n-config/locale-resources/ja-JP.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/ja-JP/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/ko-KR.ts
Normal file
1
web/i18n-config/locale-resources/ko-KR.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/ko-KR/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/nl-NL.ts
Normal file
1
web/i18n-config/locale-resources/nl-NL.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/nl-NL/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/pl-PL.ts
Normal file
1
web/i18n-config/locale-resources/pl-PL.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/pl-PL/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/pt-BR.ts
Normal file
1
web/i18n-config/locale-resources/pt-BR.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/pt-BR/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/ro-RO.ts
Normal file
1
web/i18n-config/locale-resources/ro-RO.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/ro-RO/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/ru-RU.ts
Normal file
1
web/i18n-config/locale-resources/ru-RU.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/ru-RU/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/sl-SI.ts
Normal file
1
web/i18n-config/locale-resources/sl-SI.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/sl-SI/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/th-TH.ts
Normal file
1
web/i18n-config/locale-resources/th-TH.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/th-TH/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/tr-TR.ts
Normal file
1
web/i18n-config/locale-resources/tr-TR.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/tr-TR/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/uk-UA.ts
Normal file
1
web/i18n-config/locale-resources/uk-UA.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/uk-UA/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/vi-VN.ts
Normal file
1
web/i18n-config/locale-resources/vi-VN.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/vi-VN/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/zh-Hans.ts
Normal file
1
web/i18n-config/locale-resources/zh-Hans.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/zh-Hans/${fileNamespace}.json`)
|
||||
1
web/i18n-config/locale-resources/zh-Hant.ts
Normal file
1
web/i18n-config/locale-resources/zh-Hant.ts
Normal file
@ -0,0 +1 @@
|
||||
export const loadResource = (fileNamespace: string) => import(`../../i18n/zh-Hant/${fileNamespace}.json`)
|
||||
@ -2,7 +2,6 @@ import type { i18n as I18nInstance, Resource, ResourceLanguage } from 'i18next'
|
||||
import type { Locale } from '.'
|
||||
import type { Namespace, NamespaceInFileName } from './resources'
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
import { kebabCase } from 'es-toolkit/compat'
|
||||
import { camelCase } from 'es-toolkit/string'
|
||||
import { createInstance } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
@ -12,6 +11,7 @@ import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import { cookies, headers } from '@/next/headers'
|
||||
import { serverOnlyContext } from '@/utils/server-only-context'
|
||||
import { i18n } from '.'
|
||||
import { loadI18nResource } from './load-resource'
|
||||
import { namespacesInFileName } from './resources'
|
||||
import { getInitOptions } from './settings'
|
||||
|
||||
@ -26,10 +26,7 @@ const getOrCreateI18next = async (lng: Locale) => {
|
||||
instance = createInstance()
|
||||
await instance
|
||||
.use(initReactI18next)
|
||||
.use(resourcesToBackend((language: Locale, namespace: Namespace | NamespaceInFileName) => {
|
||||
const fileNamespace = kebabCase(namespace)
|
||||
return import(`../i18n/${language}/${fileNamespace}.json`)
|
||||
}))
|
||||
.use(resourcesToBackend((language: Locale, namespace: Namespace | NamespaceInFileName) => loadI18nResource(language, namespace)))
|
||||
.init({
|
||||
...getInitOptions(),
|
||||
lng,
|
||||
@ -85,7 +82,7 @@ export const getResources = cache(async (lng: Locale): Promise<Resource> => {
|
||||
|
||||
await Promise.all(
|
||||
(namespacesInFileName).map(async (ns) => {
|
||||
const mod = await import(`../i18n/${lng}/${ns}.json`)
|
||||
const mod = await loadI18nResource(lng, ns)
|
||||
messages[camelCase(ns)] = mod.default
|
||||
}),
|
||||
)
|
||||
|
||||
@ -35,9 +35,9 @@ const config: KnipConfig = {
|
||||
ignoreFiles: [
|
||||
'features/agent-v2/agent-detail/configure/components/orchestrate/memory.tsx',
|
||||
'features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/option-menu.tsx',
|
||||
'i18n-config/locale-resources/*.ts',
|
||||
],
|
||||
ignoreBinaries: [
|
||||
'only-allow',
|
||||
'pbcopy',
|
||||
'which',
|
||||
],
|
||||
|
||||
@ -5,11 +5,13 @@ import type {
|
||||
ListReleasesResponse,
|
||||
PrecheckReleaseRequest,
|
||||
} from '@dify/contracts/enterprise/types.gen'
|
||||
import type { ContractRouterClient } from '@orpc/contract'
|
||||
import type { ClientLink } from '@orpc/client'
|
||||
import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'
|
||||
import type { JsonifiedClient } from '@orpc/openapi-client'
|
||||
import type { RouterUtils } from '@orpc/tanstack-query'
|
||||
import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query'
|
||||
import type { Tag } from '@/contract/console/tags'
|
||||
import type { consoleRouterContract } from '@/contract/router'
|
||||
import { createORPCClient, onError } from '@orpc/client'
|
||||
import { OpenAPILink } from '@orpc/openapi-client/fetch'
|
||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||
@ -19,13 +21,11 @@ import {
|
||||
IS_MARKETPLACE,
|
||||
MARKETPLACE_API_PREFIX,
|
||||
} from '@/config'
|
||||
import {
|
||||
consoleRouterContract,
|
||||
marketplaceRouterContract,
|
||||
} from '@/contract/router'
|
||||
import { marketplaceRouterContract } from '@/contract/marketplace'
|
||||
import { isClient } from '@/utils/client'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { request } from './base'
|
||||
import { createConsoleDynamicLink } from './console-link'
|
||||
|
||||
function getMarketplaceHeaders() {
|
||||
return new Headers({
|
||||
@ -58,6 +58,30 @@ export function getBaseURL(path: string) {
|
||||
return url
|
||||
}
|
||||
|
||||
type ConsoleClientContext = Record<never, never>
|
||||
type ConsoleClientLink = ClientLink<ConsoleClientContext>
|
||||
|
||||
function createConsoleOpenAPILink(contract: AnyContractRouter): ConsoleClientLink {
|
||||
return new OpenAPILink<ConsoleClientContext>(contract, {
|
||||
url: getBaseURL(API_PREFIX),
|
||||
fetch: (input, init) => {
|
||||
return request(
|
||||
input.url,
|
||||
init,
|
||||
{
|
||||
fetchCompat: true,
|
||||
request: input,
|
||||
},
|
||||
)
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const marketplaceLink = new OpenAPILink(marketplaceRouterContract, {
|
||||
url: MARKETPLACE_API_PREFIX,
|
||||
headers: () => (getMarketplaceHeaders()),
|
||||
@ -323,24 +347,7 @@ async function invalidateReleaseMutationQueries(
|
||||
])
|
||||
}
|
||||
|
||||
const consoleLink = new OpenAPILink(consoleRouterContract, {
|
||||
url: getBaseURL(API_PREFIX),
|
||||
fetch: (input, init) => {
|
||||
return request(
|
||||
input.url,
|
||||
init,
|
||||
{
|
||||
fetchCompat: true,
|
||||
request: input,
|
||||
},
|
||||
)
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
const consoleLink = createConsoleDynamicLink<ConsoleClientContext>(createConsoleOpenAPILink)
|
||||
|
||||
export const consoleClient: JsonifiedClient<ContractRouterClient<typeof consoleRouterContract>> = createORPCClient(consoleLink)
|
||||
|
||||
|
||||
32
web/service/console-link.ts
Normal file
32
web/service/console-link.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { ClientContext, ClientLink } from '@orpc/client'
|
||||
import type { AnyContractRouter } from '@orpc/contract'
|
||||
import type { consoleRouterContract } from '@/contract/router'
|
||||
import { DynamicLink } from '@orpc/client'
|
||||
import { loadConsoleContractForSegment } from './console-router-loader'
|
||||
|
||||
export type ConsoleRouterContract = typeof consoleRouterContract
|
||||
|
||||
export function createConsoleDynamicLink<TContext extends ClientContext>(
|
||||
createLink: (contract: AnyContractRouter) => ClientLink<TContext>,
|
||||
) {
|
||||
const routerLinkPromises = new Map<string, Promise<ClientLink<TContext>>>()
|
||||
|
||||
function getRouterLink(path: readonly string[]) {
|
||||
const segment = path[0]
|
||||
if (!segment)
|
||||
throw new Error('Console contract path is empty.')
|
||||
|
||||
let routerLinkPromise = routerLinkPromises.get(segment)
|
||||
if (!routerLinkPromise) {
|
||||
routerLinkPromise = loadConsoleContractForSegment(segment).then(createLink).catch((error) => {
|
||||
routerLinkPromises.delete(segment)
|
||||
throw error
|
||||
})
|
||||
routerLinkPromises.set(segment, routerLinkPromise)
|
||||
}
|
||||
|
||||
return routerLinkPromise
|
||||
}
|
||||
|
||||
return new DynamicLink<TContext>((_options, path) => getRouterLink(path))
|
||||
}
|
||||
51
web/service/console-router-loader.ts
Normal file
51
web/service/console-router-loader.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { AnyContractRouter } from '@orpc/contract'
|
||||
import { contractLoaders } from '@dify/contracts/api/console/orpc.gen'
|
||||
|
||||
const wrapConsoleContract = (segment: string, contract: unknown) => ({ [segment]: contract }) as AnyContractRouter
|
||||
|
||||
async function loadGeneratedConsoleContract(segment: string) {
|
||||
const loader = contractLoaders[segment as keyof typeof contractLoaders]
|
||||
if (!loader)
|
||||
return null
|
||||
|
||||
return loader() as Promise<AnyContractRouter>
|
||||
}
|
||||
|
||||
const customConsoleContractLoaders: Record<string, () => Promise<AnyContractRouter>> = {
|
||||
agent: () => import('@/contract/console/agent').then(({ agentRouterContract }) => wrapConsoleContract('agent', agentRouterContract)),
|
||||
apps: () => import('@/contract/console/apps').then(({ appsRouterContract }) => wrapConsoleContract('apps', appsRouterContract)),
|
||||
billing: () => import('@/contract/console/billing').then(({ billingRouterContract }) => wrapConsoleContract('billing', billingRouterContract)),
|
||||
enterprise: () => import('@dify/contracts/enterprise/orpc.gen').then(({ contract }) => wrapConsoleContract('enterprise', contract)),
|
||||
explore: () => import('@/contract/console/explore').then(({ exploreRouterContract }) => wrapConsoleContract('explore', exploreRouterContract)),
|
||||
files: () => import('@/contract/console/files').then(({ filesRouterContract }) => wrapConsoleContract('files', filesRouterContract)),
|
||||
modelProviders: () =>
|
||||
import('@/contract/console/model-providers').then(({ modelProvidersRouterContract }) => wrapConsoleContract('modelProviders', modelProvidersRouterContract)),
|
||||
notification: () =>
|
||||
import('@/contract/console/notification').then(({ notificationContract }) => wrapConsoleContract('notification', notificationContract)),
|
||||
notificationDismiss: () =>
|
||||
import('@/contract/console/notification').then(({ notificationDismissContract }) => wrapConsoleContract('notificationDismiss', notificationDismissContract)),
|
||||
plugins: () => import('@/contract/console/plugins').then(({ pluginsRouterContract }) => wrapConsoleContract('plugins', pluginsRouterContract)),
|
||||
rbacAccessConfig: () =>
|
||||
import('@/contract/console/access-control').then(({ rbacAccessConfigContract }) => wrapConsoleContract('rbacAccessConfig', rbacAccessConfigContract)),
|
||||
snippets: () => import('@/contract/console/snippets').then(({ snippetsRouterContract }) => wrapConsoleContract('snippets', snippetsRouterContract)),
|
||||
tags: () => import('@/contract/console/tags').then(({ tagsRouterContract }) => wrapConsoleContract('tags', tagsRouterContract)),
|
||||
triggers: () => import('@/contract/console/trigger').then(({ triggersRouterContract }) => wrapConsoleContract('triggers', triggersRouterContract)),
|
||||
trialApps: () => import('@/contract/console/try-app').then(({ trialAppsRouterContract }) => wrapConsoleContract('trialApps', trialAppsRouterContract)),
|
||||
workflowComments: () =>
|
||||
import('@/contract/console/workflow-comment').then(({ workflowCommentContracts }) => wrapConsoleContract('workflowComments', workflowCommentContracts)),
|
||||
workflowDraft: () =>
|
||||
import('@/contract/console/workflow').then(({ workflowDraftRouterContract }) => wrapConsoleContract('workflowDraft', workflowDraftRouterContract)),
|
||||
workspaces: () => import('@/contract/console/workspaces').then(({ workspacesRouterContract }) => wrapConsoleContract('workspaces', workspacesRouterContract)),
|
||||
}
|
||||
|
||||
export async function loadConsoleContractForSegment(segment: string) {
|
||||
const customContractLoader = customConsoleContractLoaders[segment]
|
||||
if (customContractLoader)
|
||||
return customContractLoader()
|
||||
|
||||
const generatedContract = await loadGeneratedConsoleContract(segment)
|
||||
if (generatedContract)
|
||||
return generatedContract
|
||||
|
||||
throw new Error(`Console contract segment "${segment}" is not configured.`)
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
import type { ContractRouterClient } from '@orpc/contract'
|
||||
import type { ClientLink } from '@orpc/client'
|
||||
import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'
|
||||
import type { JsonifiedClient } from '@orpc/openapi-client'
|
||||
import type { consoleRouterContract } from '@/contract/router'
|
||||
import { createORPCClient, onError } from '@orpc/client'
|
||||
import { OpenAPILink } from '@orpc/openapi-client/fetch'
|
||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||
@ -9,7 +11,7 @@ import {
|
||||
CSRF_HEADER_NAME,
|
||||
} from '@/config'
|
||||
import { SERVER_CONSOLE_API_PREFIX } from '@/config/server'
|
||||
import { consoleRouterContract } from '@/contract/router'
|
||||
import { createConsoleDynamicLink } from './console-link'
|
||||
|
||||
import 'server-only'
|
||||
|
||||
@ -68,6 +70,29 @@ const createServerConsoleRequestHeaders = (context: ServerConsoleClientContext |
|
||||
return requestHeaders
|
||||
}
|
||||
|
||||
type ServerConsoleClientLink = ClientLink<ServerConsoleClientContext>
|
||||
|
||||
function createServerConsoleOpenAPILink(contract: AnyContractRouter): ServerConsoleClientLink {
|
||||
return new OpenAPILink<ServerConsoleClientContext>(contract, {
|
||||
url: getServerConsoleApiPrefix,
|
||||
headers: ({ context }) => createServerConsoleRequestHeaders(context),
|
||||
fetch: (request, init) => {
|
||||
if (request.body && !request.headers.has('content-type'))
|
||||
request.headers.set('Content-Type', 'application/json')
|
||||
|
||||
return globalThis.fetch(request, {
|
||||
...init,
|
||||
cache: 'no-store',
|
||||
})
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const getServerConsoleClientContext = async (): Promise<ServerConsoleClientContext> => {
|
||||
const { cookies, headers } = await import('@/next/headers')
|
||||
const requestHeaders = await headers()
|
||||
@ -82,24 +107,7 @@ export const getServerConsoleClientContext = async (): Promise<ServerConsoleClie
|
||||
export const getServerConsoleRequestHeaders = async () =>
|
||||
createServerConsoleRequestHeaders(await getServerConsoleClientContext())
|
||||
|
||||
const serverConsoleLink = new OpenAPILink<ServerConsoleClientContext>(consoleRouterContract, {
|
||||
url: getServerConsoleApiPrefix,
|
||||
headers: ({ context }) => createServerConsoleRequestHeaders(context),
|
||||
fetch: (request, init) => {
|
||||
if (request.body && !request.headers.has('content-type'))
|
||||
request.headers.set('Content-Type', 'application/json')
|
||||
|
||||
return globalThis.fetch(request, {
|
||||
...init,
|
||||
cache: 'no-store',
|
||||
})
|
||||
},
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
console.error(error)
|
||||
}),
|
||||
],
|
||||
})
|
||||
const serverConsoleLink = createConsoleDynamicLink<ServerConsoleClientContext>(createServerConsoleOpenAPILink)
|
||||
|
||||
export const serverConsoleClient: JsonifiedClient<ContractRouterClient<typeof consoleRouterContract, ServerConsoleClientContext>> = createORPCClient(serverConsoleLink)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user