mirror of
https://github.com/langgenius/dify.git
synced 2026-04-16 18:46:14 +08:00
Compare commits
10 Commits
feat/trigg
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
| f5528f2030 | |||
| 6efdc94661 | |||
| 68526c09fc | |||
| a78bc507c0 | |||
| e83c7438cb | |||
| 82068a6918 | |||
| 108bcbeb7c | |||
| c4b02be6d3 | |||
| 30eebf804f | |||
| ad7fdd18d0 |
5
.github/workflows/autofix.yml
vendored
5
.github/workflows/autofix.yml
vendored
@ -28,6 +28,11 @@ jobs:
|
||||
# Format code
|
||||
uv run ruff format ..
|
||||
|
||||
- name: count migration progress
|
||||
run: |
|
||||
cd api
|
||||
./cnt_base.sh
|
||||
|
||||
- name: ast-grep
|
||||
run: |
|
||||
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||
|
||||
5
.github/workflows/vdb-tests.yml
vendored
5
.github/workflows/vdb-tests.yml
vendored
@ -1,7 +1,10 @@
|
||||
name: Run VDB Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'api/core/rag/*.py'
|
||||
|
||||
concurrency:
|
||||
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
||||
|
||||
@ -159,8 +159,7 @@ SUPABASE_URL=your-server-url
|
||||
# CORS configuration
|
||||
WEB_API_CORS_ALLOW_ORIGINS=http://localhost:3000,*
|
||||
CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,*
|
||||
# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains.
|
||||
# Provide the registrable domain (e.g. example.com); leading dots are optional.
|
||||
# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). Leading dots are optional.
|
||||
COOKIE_DOMAIN=
|
||||
|
||||
# Vector database configuration
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). The frontend and backend must be under the same top-level domain in order to share authentication cookies.
|
||||
|
||||
1. Generate a `SECRET_KEY` in the `.env` file.
|
||||
|
||||
bash for Linux
|
||||
|
||||
7
api/cnt_base.sh
Executable file
7
api/cnt_base.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
for pattern in "Base" "TypeBase"; do
|
||||
printf "%s " "$pattern"
|
||||
grep "($pattern):" -r --include='*.py' --exclude-dir=".venv" --exclude-dir="tests" . | wc -l
|
||||
done
|
||||
@ -192,7 +192,6 @@ class GraphEngine:
|
||||
self._dispatcher = Dispatcher(
|
||||
event_queue=self._event_queue,
|
||||
event_handler=self._event_handler_registry,
|
||||
event_collector=self._event_manager,
|
||||
execution_coordinator=self._execution_coordinator,
|
||||
event_emitter=self._event_manager,
|
||||
)
|
||||
|
||||
@ -43,7 +43,6 @@ class Dispatcher:
|
||||
self,
|
||||
event_queue: queue.Queue[GraphNodeEventBase],
|
||||
event_handler: "EventHandler",
|
||||
event_collector: EventManager,
|
||||
execution_coordinator: ExecutionCoordinator,
|
||||
event_emitter: EventManager | None = None,
|
||||
) -> None:
|
||||
@ -53,13 +52,11 @@ class Dispatcher:
|
||||
Args:
|
||||
event_queue: Queue of events from workers
|
||||
event_handler: Event handler registry for processing events
|
||||
event_collector: Event manager for collecting unhandled events
|
||||
execution_coordinator: Coordinator for execution flow
|
||||
event_emitter: Optional event manager to signal completion
|
||||
"""
|
||||
self._event_queue = event_queue
|
||||
self._event_handler = event_handler
|
||||
self._event_collector = event_collector
|
||||
self._execution_coordinator = execution_coordinator
|
||||
self._event_emitter = event_emitter
|
||||
|
||||
@ -86,37 +83,31 @@ class Dispatcher:
|
||||
def _dispatcher_loop(self) -> None:
|
||||
"""Main dispatcher loop."""
|
||||
try:
|
||||
self._process_commands()
|
||||
while not self._stop_event.is_set():
|
||||
commands_checked = False
|
||||
should_check_commands = False
|
||||
should_break = False
|
||||
if (
|
||||
self._execution_coordinator.aborted
|
||||
or self._execution_coordinator.paused
|
||||
or self._execution_coordinator.execution_complete
|
||||
):
|
||||
break
|
||||
|
||||
if self._execution_coordinator.is_execution_complete():
|
||||
should_check_commands = True
|
||||
should_break = True
|
||||
else:
|
||||
# Check for scaling
|
||||
self._execution_coordinator.check_scaling()
|
||||
self._execution_coordinator.check_scaling()
|
||||
try:
|
||||
event = self._event_queue.get(timeout=0.1)
|
||||
self._event_handler.dispatch(event)
|
||||
self._event_queue.task_done()
|
||||
self._process_commands(event)
|
||||
except queue.Empty:
|
||||
time.sleep(0.1)
|
||||
|
||||
# Process events
|
||||
try:
|
||||
event = self._event_queue.get(timeout=0.1)
|
||||
# Route to the event handler
|
||||
self._event_handler.dispatch(event)
|
||||
should_check_commands = self._should_check_commands(event)
|
||||
self._event_queue.task_done()
|
||||
except queue.Empty:
|
||||
# Process commands even when no new events arrive so abort requests are not missed
|
||||
should_check_commands = True
|
||||
time.sleep(0.1)
|
||||
|
||||
if should_check_commands and not commands_checked:
|
||||
self._execution_coordinator.check_commands()
|
||||
commands_checked = True
|
||||
|
||||
if should_break:
|
||||
if not commands_checked:
|
||||
self._execution_coordinator.check_commands()
|
||||
self._process_commands()
|
||||
while True:
|
||||
try:
|
||||
event = self._event_queue.get(block=False)
|
||||
self._event_handler.dispatch(event)
|
||||
self._event_queue.task_done()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
@ -129,6 +120,6 @@ class Dispatcher:
|
||||
if self._event_emitter:
|
||||
self._event_emitter.mark_complete()
|
||||
|
||||
def _should_check_commands(self, event: GraphNodeEventBase) -> bool:
|
||||
"""Return True if the event represents a node completion."""
|
||||
return isinstance(event, self._COMMAND_TRIGGER_EVENTS)
|
||||
def _process_commands(self, event: GraphNodeEventBase | None = None):
|
||||
if event is None or isinstance(event, self._COMMAND_TRIGGER_EVENTS):
|
||||
self._execution_coordinator.process_commands()
|
||||
|
||||
@ -40,7 +40,7 @@ class ExecutionCoordinator:
|
||||
self._command_processor = command_processor
|
||||
self._worker_pool = worker_pool
|
||||
|
||||
def check_commands(self) -> None:
|
||||
def process_commands(self) -> None:
|
||||
"""Process any pending commands."""
|
||||
self._command_processor.process_commands()
|
||||
|
||||
@ -48,24 +48,16 @@ class ExecutionCoordinator:
|
||||
"""Check and perform worker scaling if needed."""
|
||||
self._worker_pool.check_and_scale()
|
||||
|
||||
def is_execution_complete(self) -> bool:
|
||||
"""
|
||||
Check if execution is complete.
|
||||
|
||||
Returns:
|
||||
True if execution is complete
|
||||
"""
|
||||
# Treat paused, aborted, or failed executions as terminal states
|
||||
if self._graph_execution.is_paused:
|
||||
return True
|
||||
|
||||
if self._graph_execution.aborted or self._graph_execution.has_error:
|
||||
return True
|
||||
|
||||
@property
|
||||
def execution_complete(self):
|
||||
return self._state_manager.is_execution_complete()
|
||||
|
||||
@property
|
||||
def is_paused(self) -> bool:
|
||||
def aborted(self):
|
||||
return self._graph_execution.aborted or self._graph_execution.has_error
|
||||
|
||||
@property
|
||||
def paused(self) -> bool:
|
||||
"""Expose whether the underlying graph execution is paused."""
|
||||
return self._graph_execution.is_paused
|
||||
|
||||
|
||||
@ -38,12 +38,6 @@ class EmailType(StrEnum):
|
||||
EMAIL_REGISTER = auto()
|
||||
EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto()
|
||||
RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = auto()
|
||||
TRIGGER_EVENTS_LIMIT_SANDBOX = auto()
|
||||
TRIGGER_EVENTS_LIMIT_PROFESSIONAL = auto()
|
||||
TRIGGER_EVENTS_USAGE_WARNING_SANDBOX = auto()
|
||||
TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL = auto()
|
||||
API_RATE_LIMIT_LIMIT_SANDBOX = auto()
|
||||
API_RATE_LIMIT_WARNING_SANDBOX = auto()
|
||||
|
||||
|
||||
class EmailLanguage(StrEnum):
|
||||
@ -451,78 +445,6 @@ def create_default_email_config() -> EmailI18nConfig:
|
||||
branded_template_path="clean_document_job_mail_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.TRIGGER_EVENTS_LIMIT_SANDBOX: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’ve reached your Sandbox Trigger Events limit",
|
||||
template_path="trigger_events_limit_template_en-US.html",
|
||||
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的 Sandbox 触发事件额度已用尽",
|
||||
template_path="trigger_events_limit_template_zh-CN.html",
|
||||
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.TRIGGER_EVENTS_LIMIT_PROFESSIONAL: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’ve reached your monthly Trigger Events limit",
|
||||
template_path="trigger_events_limit_template_en-US.html",
|
||||
branded_template_path="without-brand/trigger_events_limit_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的月度触发事件额度已用尽",
|
||||
template_path="trigger_events_limit_template_zh-CN.html",
|
||||
branded_template_path="without-brand/trigger_events_limit_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.TRIGGER_EVENTS_USAGE_WARNING_SANDBOX: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’re nearing your Sandbox Trigger Events limit",
|
||||
template_path="trigger_events_usage_warning_template_en-US.html",
|
||||
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的 Sandbox 触发事件额度接近上限",
|
||||
template_path="trigger_events_usage_warning_template_zh-CN.html",
|
||||
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.TRIGGER_EVENTS_USAGE_WARNING_PROFESSIONAL: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’re nearing your Monthly Trigger Events limit",
|
||||
template_path="trigger_events_usage_warning_template_en-US.html",
|
||||
branded_template_path="without-brand/trigger_events_usage_warning_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的月度触发事件额度接近上限",
|
||||
template_path="trigger_events_usage_warning_template_zh-CN.html",
|
||||
branded_template_path="without-brand/trigger_events_usage_warning_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.API_RATE_LIMIT_LIMIT_SANDBOX: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’ve reached your API Rate Limit",
|
||||
template_path="api_rate_limit_limit_template_en-US.html",
|
||||
branded_template_path="without-brand/api_rate_limit_limit_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的 API 速率额度已用尽",
|
||||
template_path="api_rate_limit_limit_template_zh-CN.html",
|
||||
branded_template_path="without-brand/api_rate_limit_limit_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.API_RATE_LIMIT_WARNING_SANDBOX: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You’re nearing your API Rate Limit",
|
||||
template_path="api_rate_limit_warning_template_en-US.html",
|
||||
branded_template_path="without-brand/api_rate_limit_warning_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的 API 速率额度接近上限",
|
||||
template_path="api_rate_limit_warning_template_zh-CN.html",
|
||||
branded_template_path="without-brand/api_rate_limit_warning_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.EMAIL_REGISTER: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Register Your {application_title} Account",
|
||||
|
||||
@ -225,7 +225,7 @@ class Dataset(Base):
|
||||
ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id
|
||||
)
|
||||
)
|
||||
if not external_knowledge_api:
|
||||
if external_knowledge_api is None or external_knowledge_api.settings is None:
|
||||
return None
|
||||
return {
|
||||
"external_knowledge_id": external_knowledge_binding.external_knowledge_id,
|
||||
@ -945,18 +945,20 @@ class DatasetQuery(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=sa.func.current_timestamp())
|
||||
|
||||
|
||||
class DatasetKeywordTable(Base):
|
||||
class DatasetKeywordTable(TypeBase):
|
||||
__tablename__ = "dataset_keyword_tables"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="dataset_keyword_table_pkey"),
|
||||
sa.Index("dataset_keyword_table_dataset_id_idx", "dataset_id"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"))
|
||||
dataset_id = mapped_column(StringUUID, nullable=False, unique=True)
|
||||
keyword_table = mapped_column(sa.Text, nullable=False)
|
||||
data_source_type = mapped_column(
|
||||
String(255), nullable=False, server_default=sa.text("'database'::character varying")
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"), init=False
|
||||
)
|
||||
dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False, unique=True)
|
||||
keyword_table: Mapped[str] = mapped_column(sa.Text, nullable=False)
|
||||
data_source_type: Mapped[str] = mapped_column(
|
||||
String(255), nullable=False, server_default=sa.text("'database'::character varying"), default="database"
|
||||
)
|
||||
|
||||
@property
|
||||
@ -1054,19 +1056,23 @@ class TidbAuthBinding(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class Whitelist(Base):
|
||||
class Whitelist(TypeBase):
|
||||
__tablename__ = "whitelists"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="whitelists_pkey"),
|
||||
sa.Index("whitelists_tenant_idx", "tenant_id"),
|
||||
)
|
||||
id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=True)
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"), init=False
|
||||
)
|
||||
tenant_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
category: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
|
||||
)
|
||||
|
||||
|
||||
class DatasetPermission(Base):
|
||||
class DatasetPermission(TypeBase):
|
||||
__tablename__ = "dataset_permissions"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="dataset_permission_pkey"),
|
||||
@ -1075,15 +1081,21 @@ class DatasetPermission(Base):
|
||||
sa.Index("idx_dataset_permissions_tenant_id", "tenant_id"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), primary_key=True)
|
||||
dataset_id = mapped_column(StringUUID, nullable=False)
|
||||
account_id = mapped_column(StringUUID, nullable=False)
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
has_permission: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, server_default=sa.text("uuid_generate_v4()"), primary_key=True, init=False
|
||||
)
|
||||
dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
has_permission: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("true"), default=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
|
||||
)
|
||||
|
||||
|
||||
class ExternalKnowledgeApis(Base):
|
||||
class ExternalKnowledgeApis(TypeBase):
|
||||
__tablename__ = "external_knowledge_apis"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="external_knowledge_apis_pkey"),
|
||||
@ -1091,16 +1103,20 @@ class ExternalKnowledgeApis(Base):
|
||||
sa.Index("external_knowledge_apis_name_idx", "name"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"), init=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
settings = mapped_column(sa.Text, nullable=True)
|
||||
created_by = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
settings: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
|
||||
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
|
||||
)
|
||||
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), init=False
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@ -1178,7 +1194,7 @@ class DatasetAutoDisableLog(Base):
|
||||
)
|
||||
|
||||
|
||||
class RateLimitLog(Base):
|
||||
class RateLimitLog(TypeBase):
|
||||
__tablename__ = "rate_limit_logs"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"),
|
||||
@ -1186,12 +1202,12 @@ class RateLimitLog(Base):
|
||||
sa.Index("rate_limit_log_operation_idx", "operation"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
|
||||
tenant_id = mapped_column(StringUUID, nullable=False)
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"), init=False)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
subscription_plan: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
operation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)")
|
||||
DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)"), init=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from core.trigger.entities.api_entities import TriggerProviderSubscriptionApiEnt
|
||||
from core.trigger.entities.entities import Subscription
|
||||
from core.trigger.utils.endpoint import generate_plugin_trigger_endpoint_url, generate_webhook_trigger_endpoint
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.base import Base
|
||||
from models.base import Base, TypeBase
|
||||
from models.engine import db
|
||||
from models.enums import AppTriggerStatus, AppTriggerType, CreatorUserRole, WorkflowTriggerStatus
|
||||
from models.model import Account
|
||||
@ -399,7 +399,7 @@ class AppTrigger(Base):
|
||||
)
|
||||
|
||||
|
||||
class WorkflowSchedulePlan(Base):
|
||||
class WorkflowSchedulePlan(TypeBase):
|
||||
"""
|
||||
Workflow Schedule Configuration
|
||||
|
||||
@ -425,7 +425,7 @@ class WorkflowSchedulePlan(Base):
|
||||
sa.Index("workflow_schedule_plan_next_idx", "next_run_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuidv7()"))
|
||||
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuidv7()"), init=False)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
node_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
@ -436,9 +436,11 @@ class WorkflowSchedulePlan(Base):
|
||||
|
||||
# Schedule control
|
||||
next_run_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), init=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), init=False
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
|
||||
@ -62,7 +62,7 @@ class ExternalDatasetService:
|
||||
tenant_id=tenant_id,
|
||||
created_by=user_id,
|
||||
updated_by=user_id,
|
||||
name=args.get("name"),
|
||||
name=str(args.get("name")),
|
||||
description=args.get("description", ""),
|
||||
settings=json.dumps(args.get("settings"), ensure_ascii=False),
|
||||
)
|
||||
@ -163,7 +163,7 @@ class ExternalDatasetService:
|
||||
external_knowledge_api = (
|
||||
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
|
||||
)
|
||||
if external_knowledge_api is None:
|
||||
if external_knowledge_api is None or external_knowledge_api.settings is None:
|
||||
raise ValueError("api template not found")
|
||||
settings = json.loads(external_knowledge_api.settings)
|
||||
for setting in settings:
|
||||
@ -290,7 +290,7 @@ class ExternalDatasetService:
|
||||
.filter_by(id=external_knowledge_binding.external_knowledge_api_id)
|
||||
.first()
|
||||
)
|
||||
if not external_knowledge_api:
|
||||
if external_knowledge_api is None or external_knowledge_api.settings is None:
|
||||
raise ValueError("external api template not found")
|
||||
|
||||
settings = json.loads(external_knowledge_api.settings)
|
||||
|
||||
@ -13,13 +13,13 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
||||
from core.app.apps.workflow.app_generator import SKIP_PREPARE_USER_INPUTS_KEY, WorkflowAppGenerator
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.app.layers.timeslice_layer import TimeSliceLayer
|
||||
from core.app.layers.trigger_post_layer import TriggerPostLayer
|
||||
from extensions.ext_database import db
|
||||
from models.account import Account
|
||||
from models.enums import CreatorUserRole, WorkflowTriggerStatus
|
||||
from models.enums import AppTriggerType, CreatorUserRole, WorkflowTriggerStatus
|
||||
from models.model import App, EndUser, Tenant
|
||||
from models.trigger import WorkflowTriggerLog
|
||||
from models.workflow import Workflow
|
||||
@ -81,6 +81,19 @@ def execute_workflow_sandbox(task_data_dict: dict[str, Any]):
|
||||
)
|
||||
|
||||
|
||||
def _build_generator_args(trigger_data: TriggerData) -> dict[str, Any]:
|
||||
"""Build args passed into WorkflowAppGenerator.generate for Celery executions."""
|
||||
args: dict[str, Any] = {
|
||||
"inputs": dict(trigger_data.inputs),
|
||||
"files": list(trigger_data.files),
|
||||
}
|
||||
|
||||
if trigger_data.trigger_type == AppTriggerType.TRIGGER_WEBHOOK:
|
||||
args[SKIP_PREPARE_USER_INPUTS_KEY] = True # Webhooks already provide structured inputs
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _execute_workflow_common(
|
||||
task_data: WorkflowTaskData,
|
||||
cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler,
|
||||
@ -128,7 +141,7 @@ def _execute_workflow_common(
|
||||
generator = WorkflowAppGenerator()
|
||||
|
||||
# Prepare args matching AppGenerateService.generate format
|
||||
args: dict[str, Any] = {"inputs": dict(trigger_data.inputs), "files": list(trigger_data.files)}
|
||||
args = _build_generator_args(trigger_data)
|
||||
|
||||
# If workflow_id was specified, add it to args
|
||||
if trigger_data.workflow_id:
|
||||
|
||||
@ -9,7 +9,7 @@ from core.rag.index_processor.index_processor_factory import IndexProcessorFacto
|
||||
from core.tools.utils.web_reader_tool import get_image_upload_file_ids
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_storage import storage
|
||||
from models.dataset import Dataset, DocumentSegment
|
||||
from models.dataset import Dataset, DatasetMetadataBinding, DocumentSegment
|
||||
from models.model import UploadFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -37,6 +37,11 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form
|
||||
if not dataset:
|
||||
raise Exception("Document has no dataset")
|
||||
|
||||
db.session.query(DatasetMetadataBinding).where(
|
||||
DatasetMetadataBinding.dataset_id == dataset_id,
|
||||
DatasetMetadataBinding.document_id.in_(document_ids),
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
segments = db.session.scalars(
|
||||
select(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids))
|
||||
).all()
|
||||
@ -71,7 +76,8 @@ def batch_clean_document_task(document_ids: list[str], dataset_id: str, doc_form
|
||||
except Exception:
|
||||
logger.exception("Delete file failed when document deleted, file_id: %s", file.id)
|
||||
db.session.delete(file)
|
||||
db.session.commit()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logger.info(
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 434px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’ve reached your API Rate Limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>Monthly API Rate Limit</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
As a result, API access has been temporarily paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To continue using the Dify API and unlock a higher limit, please upgrade to a paid plan.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,177 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 434px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的 API 速率额度已用尽</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>月度 API 速率额度</strong>,触及
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
因此,API 访问已被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
若要继续使用 Dify API 并解锁更高额度,请升级到付费套餐。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,179 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’re nearing your API Rate Limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used <strong>80% of its Monthly API Rate Limit</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Once the limit is reached, API access will be temporarily paused until the next monthly reset.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To avoid service interruptions and ensure continued access to the Dify API, please consider upgrading your plan for a higher API
|
||||
Rate Limit.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,177 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的 API 速率额度接近上限</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>80% 的月度 API 速率额度</strong>,触及
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
一旦达到上限,API 访问将暂停,直至下一个月度重置。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
为避免服务中断并持续访问 Dify API,请考虑升级到额度更高的套餐。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,184 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’ve reached your trigger events limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>{{usageScope | default('Trigger Events')}}</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Workflows triggered by <strong>{{triggerSources}}</strong> events have been temporarily paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To keep your workflows running without interruption, please upgrade your plan to unlock more Trigger Events.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
Trigger Events for the {{planName}} Plan {{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,184 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的触发事件额度已用尽</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>{{usageScope | default('触发事件额度')}}</strong>,并耗尽
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong> 的全部额度。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
由 <strong>{{triggerSources}}</strong> 触发的工作流已被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
为保证工作流不中断,请升级套餐以解锁更多触发事件额度。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
{{planName}} 计划的触发事件额度{{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,185 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’re nearing your Trigger Events limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used <strong>{{usagePercent}}</strong> of its
|
||||
<strong>{{usageScope}}</strong> for the <strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Once the limit is reached, workflows triggered by <strong>{{triggerSources}}</strong> events will be temporarily
|
||||
paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
{{upgradeHint}}
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
Trigger Events for the {{planName}} Plan {{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,184 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-header img {
|
||||
width: 68px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的触发事件额度接近上限</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>{{usagePercent}}</strong> 的
|
||||
<strong>{{usageScope}}</strong>,触及 <strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
一旦达到上限,由 <strong>{{triggerSources}}</strong> 触发的工作流将被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
{{upgradeHint}}
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
{{planName}} 计划的触发事件额度{{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,173 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 434px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’ve reached your API Rate Limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>Monthly API Rate Limit</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
As a result, API access has been temporarily paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To continue using the Dify API and unlock a higher limit, please upgrade to a paid plan.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,172 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 434px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的 API 速率额度已用尽</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>月度 API 速率额度</strong>,触及
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
因此,API 访问已被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
若要继续使用 Dify API 并解锁更高额度,请升级到付费套餐。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,174 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’re nearing your API Rate Limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used <strong>80% of its Monthly API Rate Limit</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Once the limit is reached, API access will be temporarily paused until the next monthly reset.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To avoid service interruptions and ensure continued access to the Dify API, please consider upgrading your plan for a higher API
|
||||
Rate Limit.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>Monthly API Rate Limit</strong> for the <strong>{{planName}} Plan</strong> will reset on <strong>{{resetDate}}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,172 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的 API 速率额度接近上限</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>80% 的月度 API 速率额度</strong>,触及
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
一旦达到上限,API 访问将暂停,直至下一个月度重置。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
为避免服务中断并持续访问 Dify API,请考虑升级到额度更高的套餐。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>{{planName}} 计划的月度 API 速率额度</strong> 将于 <strong>{{resetDate}}</strong> 重置。</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,179 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’ve reached your trigger events limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used all available <strong>{{usageScope | default('Trigger Events')}}</strong> for the
|
||||
<strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Workflows triggered by <strong>{{triggerSources}}</strong> events have been temporarily paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
To keep your workflows running without interruption, please upgrade your plan to unlock more Trigger Events.
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
Trigger Events for the {{planName}} Plan {{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,179 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的触发事件额度已用尽</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已用完 <strong>{{usageScope | default('触发事件额度')}}</strong>,并耗尽
|
||||
<strong>{{planName}} 计划(上限:{{planLimit}})</strong> 的全部额度。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
由 <strong>{{triggerSources}}</strong> 触发的工作流已被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
为保证工作流不中断,请升级套餐以解锁更多触发事件额度。
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
{{planName}} 计划的触发事件额度{{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,180 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">You’re nearing your Trigger Events limit</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>Dear {{ recipientName | default(workspaceName ~ ' team', true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Your workspace <strong>{{workspaceName}}</strong> has used <strong>{{usagePercent}}</strong> of its
|
||||
<strong>{{usageScope}}</strong> for the <strong>{{planName}} Plan (limit: {{planLimit}})</strong>.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
Once the limit is reached, workflows triggered by <strong>{{triggerSources}}</strong> events will be temporarily
|
||||
paused.
|
||||
</p>
|
||||
<p class="body-text">
|
||||
{{upgradeHint}}
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
Trigger Events for the {{planName}} Plan {{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">Best regards,</p>
|
||||
<p class="signature-text">The Dify Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">Please do not reply directly to this email, it is automatically sent by the system.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,179 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
background-color: #e9ebf0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 600px;
|
||||
min-height: 454px;
|
||||
margin: 40px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #ffffff;
|
||||
box-shadow: 0px 3px 10px -2px rgba(9, 9, 11, 0.08), 0px 2px 4px -2px rgba(9, 9, 11, 0.06);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
padding: 36px 48px 24px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
padding: 8px 48px 48px 48px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 120%;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.body-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.body-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.body-text strong {
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 504px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff !important;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signature-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
color: #354052;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
margin: 20px auto 40px;
|
||||
max-width: 600px;
|
||||
color: #676f83;
|
||||
text-align: center;
|
||||
font-family: 'Inter', 'PingFang SC', 'Arial', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.06px;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h1 class="title">您的触发事件额度接近上限</h1>
|
||||
<div class="body-group">
|
||||
<p class="body-text">
|
||||
<strong>亲爱的 {{ recipientName | default(workspaceName, true) }},</strong>
|
||||
</p>
|
||||
<p class="body-text">
|
||||
您的工作区 <strong>{{workspaceName}}</strong> 已使用 <strong>{{usagePercent}}</strong> 的
|
||||
<strong>{{usageScope}}</strong>,触及 <strong>{{planName}} 计划(上限:{{planLimit}})</strong>。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
一旦达到上限,由 <strong>{{triggerSources}}</strong> 触发的工作流将被暂时暂停。
|
||||
</p>
|
||||
<p class="body-text">
|
||||
{{upgradeHint}}
|
||||
</p>
|
||||
<a class="cta" href="{{upgradeUrl}}" target="_blank" rel="noopener noreferrer">{{ctaLabel}}</a>
|
||||
<p class="note">
|
||||
<strong>
|
||||
{% if resetLine is defined %}
|
||||
{{ resetLine }}
|
||||
{% else %}
|
||||
{{planName}} 计划的触发事件额度{{resetDescription}}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="signature">
|
||||
<p class="signature-text">此致敬礼,</p>
|
||||
<p class="signature-text">Dify 团队</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="email-footer">请勿直接回复此邮件,该邮件由系统自动发送。</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1,189 @@
|
||||
"""Tests for dispatcher command checking behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
from datetime import datetime
|
||||
from unittest import mock
|
||||
|
||||
from core.workflow.entities.pause_reason import SchedulingPause
|
||||
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
|
||||
from core.workflow.graph_engine.event_management.event_handlers import EventHandler
|
||||
from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
|
||||
from core.workflow.graph_engine.orchestration.execution_coordinator import ExecutionCoordinator
|
||||
from core.workflow.graph_events import (
|
||||
GraphNodeEventBase,
|
||||
NodeRunPauseRequestedEvent,
|
||||
NodeRunStartedEvent,
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
from core.workflow.node_events import NodeRunResult
|
||||
|
||||
|
||||
def test_dispatcher_should_consume_remains_events_after_pause():
|
||||
event_queue = queue.Queue()
|
||||
event_queue.put(
|
||||
GraphNodeEventBase(
|
||||
id="test",
|
||||
node_id="test",
|
||||
node_type=NodeType.START,
|
||||
)
|
||||
)
|
||||
event_handler = mock.Mock(spec=EventHandler)
|
||||
execution_coordinator = mock.Mock(spec=ExecutionCoordinator)
|
||||
execution_coordinator.paused.return_value = True
|
||||
dispatcher = Dispatcher(
|
||||
event_queue=event_queue,
|
||||
event_handler=event_handler,
|
||||
execution_coordinator=execution_coordinator,
|
||||
)
|
||||
dispatcher._dispatcher_loop()
|
||||
assert event_queue.empty()
|
||||
|
||||
|
||||
class _StubExecutionCoordinator:
|
||||
"""Stub execution coordinator that tracks command checks."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.command_checks = 0
|
||||
self.scaling_checks = 0
|
||||
self.execution_complete = False
|
||||
self.failed = False
|
||||
self._paused = False
|
||||
|
||||
def process_commands(self) -> None:
|
||||
self.command_checks += 1
|
||||
|
||||
def check_scaling(self) -> None:
|
||||
self.scaling_checks += 1
|
||||
|
||||
@property
|
||||
def paused(self) -> bool:
|
||||
return self._paused
|
||||
|
||||
@property
|
||||
def aborted(self) -> bool:
|
||||
return False
|
||||
|
||||
def mark_complete(self) -> None:
|
||||
self.execution_complete = True
|
||||
|
||||
def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
|
||||
self.failed = True
|
||||
|
||||
|
||||
class _StubEventHandler:
|
||||
"""Minimal event handler that marks execution complete after handling an event."""
|
||||
|
||||
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
|
||||
self._coordinator = coordinator
|
||||
self.events = []
|
||||
|
||||
def dispatch(self, event) -> None:
|
||||
self.events.append(event)
|
||||
self._coordinator.mark_complete()
|
||||
|
||||
|
||||
def _run_dispatcher_for_event(event) -> int:
|
||||
"""Run the dispatcher loop for a single event and return command check count."""
|
||||
event_queue: queue.Queue = queue.Queue()
|
||||
event_queue.put(event)
|
||||
|
||||
coordinator = _StubExecutionCoordinator()
|
||||
event_handler = _StubEventHandler(coordinator)
|
||||
|
||||
dispatcher = Dispatcher(
|
||||
event_queue=event_queue,
|
||||
event_handler=event_handler,
|
||||
execution_coordinator=coordinator,
|
||||
)
|
||||
|
||||
dispatcher._dispatcher_loop()
|
||||
|
||||
return coordinator.command_checks
|
||||
|
||||
|
||||
def _make_started_event() -> NodeRunStartedEvent:
|
||||
return NodeRunStartedEvent(
|
||||
id="start-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
def _make_succeeded_event() -> NodeRunSucceededEvent:
|
||||
return NodeRunSucceededEvent(
|
||||
id="success-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
|
||||
)
|
||||
|
||||
|
||||
def test_dispatcher_checks_commands_during_idle_and_on_completion() -> None:
|
||||
"""Dispatcher polls commands when idle and after completion events."""
|
||||
started_checks = _run_dispatcher_for_event(_make_started_event())
|
||||
succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
|
||||
|
||||
assert started_checks == 2
|
||||
assert succeeded_checks == 3
|
||||
|
||||
|
||||
class _PauseStubEventHandler:
|
||||
"""Minimal event handler that marks execution complete after handling an event."""
|
||||
|
||||
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
|
||||
self._coordinator = coordinator
|
||||
self.events = []
|
||||
|
||||
def dispatch(self, event) -> None:
|
||||
self.events.append(event)
|
||||
if isinstance(event, NodeRunPauseRequestedEvent):
|
||||
self._coordinator.mark_complete()
|
||||
|
||||
|
||||
def test_dispatcher_drain_event_queue():
|
||||
events = [
|
||||
NodeRunStartedEvent(
|
||||
id="start-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Code",
|
||||
start_at=datetime.utcnow(),
|
||||
),
|
||||
NodeRunPauseRequestedEvent(
|
||||
id="pause-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
reason=SchedulingPause(message="test pause"),
|
||||
),
|
||||
NodeRunSucceededEvent(
|
||||
id="success-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
start_at=datetime.utcnow(),
|
||||
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
|
||||
),
|
||||
]
|
||||
|
||||
event_queue: queue.Queue = queue.Queue()
|
||||
for e in events:
|
||||
event_queue.put(e)
|
||||
|
||||
coordinator = _StubExecutionCoordinator()
|
||||
event_handler = _PauseStubEventHandler(coordinator)
|
||||
|
||||
dispatcher = Dispatcher(
|
||||
event_queue=event_queue,
|
||||
event_handler=event_handler,
|
||||
execution_coordinator=coordinator,
|
||||
)
|
||||
|
||||
dispatcher._dispatcher_loop()
|
||||
|
||||
# ensure all events are drained.
|
||||
assert event_queue.empty()
|
||||
@ -3,13 +3,17 @@
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.workflow.entities.graph_init_params import GraphInitParams
|
||||
from core.workflow.entities.pause_reason import SchedulingPause
|
||||
from core.workflow.graph import Graph
|
||||
from core.workflow.graph_engine import GraphEngine
|
||||
from core.workflow.graph_engine.command_channels import InMemoryChannel
|
||||
from core.workflow.graph_engine.entities.commands import AbortCommand, CommandType, PauseCommand
|
||||
from core.workflow.graph_events import GraphRunAbortedEvent, GraphRunPausedEvent, GraphRunStartedEvent
|
||||
from core.workflow.nodes.start.start_node import StartNode
|
||||
from core.workflow.runtime import GraphRuntimeState, VariablePool
|
||||
from models.enums import UserFrom
|
||||
|
||||
|
||||
def test_abort_command():
|
||||
@ -26,11 +30,23 @@ def test_abort_command():
|
||||
mock_graph.root_node.id = "start"
|
||||
|
||||
# Create mock nodes with required attributes - using shared runtime state
|
||||
mock_start_node = MagicMock()
|
||||
mock_start_node.state = None
|
||||
mock_start_node.id = "start"
|
||||
mock_start_node.graph_runtime_state = shared_runtime_state # Use shared instance
|
||||
mock_graph.nodes["start"] = mock_start_node
|
||||
start_node = StartNode(
|
||||
id="start",
|
||||
config={"id": "start"},
|
||||
graph_init_params=GraphInitParams(
|
||||
tenant_id="test_tenant",
|
||||
app_id="test_app",
|
||||
workflow_id="test_workflow",
|
||||
graph_config={},
|
||||
user_id="test_user",
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.DEBUGGER,
|
||||
call_depth=0,
|
||||
),
|
||||
graph_runtime_state=shared_runtime_state,
|
||||
)
|
||||
start_node.init_node_data({"title": "start", "variables": []})
|
||||
mock_graph.nodes["start"] = start_node
|
||||
|
||||
# Mock graph methods
|
||||
mock_graph.get_outgoing_edges = MagicMock(return_value=[])
|
||||
@ -124,11 +140,23 @@ def test_pause_command():
|
||||
mock_graph.root_node = MagicMock()
|
||||
mock_graph.root_node.id = "start"
|
||||
|
||||
mock_start_node = MagicMock()
|
||||
mock_start_node.state = None
|
||||
mock_start_node.id = "start"
|
||||
mock_start_node.graph_runtime_state = shared_runtime_state
|
||||
mock_graph.nodes["start"] = mock_start_node
|
||||
start_node = StartNode(
|
||||
id="start",
|
||||
config={"id": "start"},
|
||||
graph_init_params=GraphInitParams(
|
||||
tenant_id="test_tenant",
|
||||
app_id="test_app",
|
||||
workflow_id="test_workflow",
|
||||
graph_config={},
|
||||
user_id="test_user",
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.DEBUGGER,
|
||||
call_depth=0,
|
||||
),
|
||||
graph_runtime_state=shared_runtime_state,
|
||||
)
|
||||
start_node.init_node_data({"title": "start", "variables": []})
|
||||
mock_graph.nodes["start"] = start_node
|
||||
|
||||
mock_graph.get_outgoing_edges = MagicMock(return_value=[])
|
||||
mock_graph.get_incoming_edges = MagicMock(return_value=[])
|
||||
@ -153,5 +181,5 @@ def test_pause_command():
|
||||
assert pause_events[0].reason == SchedulingPause(message="User requested pause")
|
||||
|
||||
graph_execution = engine.graph_runtime_state.graph_execution
|
||||
assert graph_execution.is_paused
|
||||
assert graph_execution.paused
|
||||
assert graph_execution.pause_reason == SchedulingPause(message="User requested pause")
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
"""Tests for dispatcher command checking behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
from datetime import datetime
|
||||
|
||||
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
|
||||
from core.workflow.graph_engine.event_management.event_manager import EventManager
|
||||
from core.workflow.graph_engine.orchestration.dispatcher import Dispatcher
|
||||
from core.workflow.graph_events import NodeRunStartedEvent, NodeRunSucceededEvent
|
||||
from core.workflow.node_events import NodeRunResult
|
||||
|
||||
|
||||
class _StubExecutionCoordinator:
|
||||
"""Stub execution coordinator that tracks command checks."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.command_checks = 0
|
||||
self.scaling_checks = 0
|
||||
self._execution_complete = False
|
||||
self.mark_complete_called = False
|
||||
self.failed = False
|
||||
self._paused = False
|
||||
|
||||
def check_commands(self) -> None:
|
||||
self.command_checks += 1
|
||||
|
||||
def check_scaling(self) -> None:
|
||||
self.scaling_checks += 1
|
||||
|
||||
@property
|
||||
def is_paused(self) -> bool:
|
||||
return self._paused
|
||||
|
||||
def is_execution_complete(self) -> bool:
|
||||
return self._execution_complete
|
||||
|
||||
def mark_complete(self) -> None:
|
||||
self.mark_complete_called = True
|
||||
|
||||
def mark_failed(self, error: Exception) -> None: # pragma: no cover - defensive, not triggered in tests
|
||||
self.failed = True
|
||||
|
||||
def set_execution_complete(self) -> None:
|
||||
self._execution_complete = True
|
||||
|
||||
|
||||
class _StubEventHandler:
|
||||
"""Minimal event handler that marks execution complete after handling an event."""
|
||||
|
||||
def __init__(self, coordinator: _StubExecutionCoordinator) -> None:
|
||||
self._coordinator = coordinator
|
||||
self.events = []
|
||||
|
||||
def dispatch(self, event) -> None:
|
||||
self.events.append(event)
|
||||
self._coordinator.set_execution_complete()
|
||||
|
||||
|
||||
def _run_dispatcher_for_event(event) -> int:
|
||||
"""Run the dispatcher loop for a single event and return command check count."""
|
||||
event_queue: queue.Queue = queue.Queue()
|
||||
event_queue.put(event)
|
||||
|
||||
coordinator = _StubExecutionCoordinator()
|
||||
event_handler = _StubEventHandler(coordinator)
|
||||
event_manager = EventManager()
|
||||
|
||||
dispatcher = Dispatcher(
|
||||
event_queue=event_queue,
|
||||
event_handler=event_handler,
|
||||
event_collector=event_manager,
|
||||
execution_coordinator=coordinator,
|
||||
)
|
||||
|
||||
dispatcher._dispatcher_loop()
|
||||
|
||||
return coordinator.command_checks
|
||||
|
||||
|
||||
def _make_started_event() -> NodeRunStartedEvent:
|
||||
return NodeRunStartedEvent(
|
||||
id="start-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
def _make_succeeded_event() -> NodeRunSucceededEvent:
|
||||
return NodeRunSucceededEvent(
|
||||
id="success-event",
|
||||
node_id="node-1",
|
||||
node_type=NodeType.CODE,
|
||||
node_title="Test Node",
|
||||
start_at=datetime.utcnow(),
|
||||
node_run_result=NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED),
|
||||
)
|
||||
|
||||
|
||||
def test_dispatcher_checks_commands_during_idle_and_on_completion() -> None:
|
||||
"""Dispatcher polls commands when idle and after completion events."""
|
||||
started_checks = _run_dispatcher_for_event(_make_started_event())
|
||||
succeeded_checks = _run_dispatcher_for_event(_make_succeeded_event())
|
||||
|
||||
assert started_checks == 1
|
||||
assert succeeded_checks == 2
|
||||
@ -48,15 +48,3 @@ def test_handle_pause_noop_when_execution_running() -> None:
|
||||
|
||||
worker_pool.stop.assert_not_called()
|
||||
state_manager.clear_executing.assert_not_called()
|
||||
|
||||
|
||||
def test_is_execution_complete_when_paused() -> None:
|
||||
"""Paused execution should be treated as complete."""
|
||||
graph_execution = GraphExecution(workflow_id="workflow")
|
||||
graph_execution.start()
|
||||
graph_execution.pause("Awaiting input")
|
||||
|
||||
coordinator, state_manager, _worker_pool = _build_coordinator(graph_execution)
|
||||
state_manager.is_execution_complete.return_value = False
|
||||
|
||||
assert coordinator.is_execution_complete()
|
||||
|
||||
37
api/tests/unit_tests/tasks/test_async_workflow_tasks.py
Normal file
37
api/tests/unit_tests/tasks/test_async_workflow_tasks.py
Normal file
@ -0,0 +1,37 @@
|
||||
from core.app.apps.workflow.app_generator import SKIP_PREPARE_USER_INPUTS_KEY
|
||||
from models.enums import AppTriggerType, WorkflowRunTriggeredFrom
|
||||
from services.workflow.entities import TriggerData, WebhookTriggerData
|
||||
from tasks import async_workflow_tasks
|
||||
|
||||
|
||||
def test_build_generator_args_sets_skip_flag_for_webhook():
|
||||
trigger_data = WebhookTriggerData(
|
||||
app_id="app",
|
||||
tenant_id="tenant",
|
||||
workflow_id="workflow",
|
||||
root_node_id="node",
|
||||
inputs={"webhook_data": {"body": {"foo": "bar"}}},
|
||||
)
|
||||
|
||||
args = async_workflow_tasks._build_generator_args(trigger_data)
|
||||
|
||||
assert args[SKIP_PREPARE_USER_INPUTS_KEY] is True
|
||||
assert args["inputs"]["webhook_data"]["body"]["foo"] == "bar"
|
||||
|
||||
|
||||
def test_build_generator_args_keeps_validation_for_other_triggers():
|
||||
trigger_data = TriggerData(
|
||||
app_id="app",
|
||||
tenant_id="tenant",
|
||||
workflow_id="workflow",
|
||||
root_node_id="node",
|
||||
inputs={"foo": "bar"},
|
||||
files=[],
|
||||
trigger_type=AppTriggerType.TRIGGER_SCHEDULE,
|
||||
trigger_from=WorkflowRunTriggeredFrom.SCHEDULE,
|
||||
)
|
||||
|
||||
args = async_workflow_tasks._build_generator_args(trigger_data)
|
||||
|
||||
assert SKIP_PREPARE_USER_INPUTS_KEY not in args
|
||||
assert args["inputs"] == {"foo": "bar"}
|
||||
@ -365,10 +365,9 @@ WEB_API_CORS_ALLOW_ORIGINS=*
|
||||
# Specifies the allowed origins for cross-origin requests to the console API,
|
||||
# e.g. https://cloud.dify.ai or * for all origins.
|
||||
CONSOLE_CORS_ALLOW_ORIGINS=*
|
||||
# Set COOKIE_DOMAIN when the console frontend and API are on different subdomains.
|
||||
# Provide the registrable domain (e.g. example.com); leading dots are optional.
|
||||
# When the frontend and backend run on different subdomains, set COOKIE_DOMAIN to the site’s top-level domain (e.g., `example.com`). Leading dots are optional.
|
||||
COOKIE_DOMAIN=
|
||||
# The frontend reads NEXT_PUBLIC_COOKIE_DOMAIN to align cookie handling with the API.
|
||||
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
|
||||
# ------------------------------
|
||||
|
||||
@ -12,6 +12,9 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
|
||||
# console or api domain.
|
||||
# example: http://udify.app/api
|
||||
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
|
||||
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
|
||||
# The API PREFIX for MARKETPLACE
|
||||
NEXT_PUBLIC_MARKETPLACE_API_PREFIX=https://marketplace.dify.ai/api/v1
|
||||
# The URL for MARKETPLACE
|
||||
@ -34,9 +37,6 @@ NEXT_PUBLIC_CSP_WHITELIST=
|
||||
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
NEXT_PUBLIC_ALLOW_EMBED=
|
||||
|
||||
# Shared cookie domain when console UI and API use different subdomains (e.g. example.com)
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
|
||||
# Allow rendering unsafe URLs which have "data:" scheme.
|
||||
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ NEXT_PUBLIC_EDITION=SELF_HOSTED
|
||||
# different from api or web app domain.
|
||||
# example: http://cloud.dify.ai/console/api
|
||||
NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
|
||||
NEXT_PUBLIC_COOKIE_DOMAIN=
|
||||
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
|
||||
# console or api domain.
|
||||
# example: http://udify.app/api
|
||||
@ -41,6 +42,11 @@ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 1. When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. The frontend and backend must be under the same top-level domain in order to share authentication cookies.
|
||||
> 1. It's necessary to set NEXT_PUBLIC_API_PREFIX and NEXT_PUBLIC_PUBLIC_API_PREFIX to the correct backend API URL.
|
||||
|
||||
Finally, run the development server:
|
||||
|
||||
```bash
|
||||
|
||||
@ -49,7 +49,6 @@ import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
@ -107,7 +106,6 @@ export type AppPublisherProps = {
|
||||
workflowToolAvailable?: boolean
|
||||
missingStartNode?: boolean
|
||||
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
|
||||
startNodeLimitExceeded?: boolean
|
||||
}
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
@ -129,7 +127,6 @@ const AppPublisher = ({
|
||||
workflowToolAvailable = true,
|
||||
missingStartNode = false,
|
||||
hasTriggerNode = false,
|
||||
startNodeLimitExceeded = false,
|
||||
}: AppPublisherProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -249,13 +246,6 @@ const AppPublisher = ({
|
||||
const hasPublishedVersion = !!publishedAt
|
||||
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
|
||||
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
|
||||
const showStartNodeLimitHint = Boolean(startNodeLimitExceeded)
|
||||
const upgradeHighlightStyle = useMemo(() => ({
|
||||
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -314,49 +304,29 @@ const AppPublisher = ({
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-3 w-full'
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: (
|
||||
<div className='flex gap-1'>
|
||||
<span>{t('workflow.common.publishUpdate')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-3 w-full'
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: (
|
||||
<div className='flex gap-1'>
|
||||
<span>{t('workflow.common.publishUpdate')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
{showStartNodeLimitHint && (
|
||||
<div className='mt-3 flex flex-col items-stretch'>
|
||||
<p
|
||||
className='text-sm font-semibold leading-5 text-transparent'
|
||||
style={upgradeHighlightStyle}
|
||||
>
|
||||
<span className='block'>{t('workflow.publishLimit.startNodeTitlePrefix')}</span>
|
||||
<span className='block'>{t('workflow.publishLimit.startNodeTitleSuffix')}</span>
|
||||
</p>
|
||||
<p className='mt-1 text-xs leading-4 text-text-secondary'>
|
||||
{t('workflow.publishLimit.startNodeDesc')}
|
||||
</p>
|
||||
<UpgradeBtn
|
||||
isShort
|
||||
className='mb-[12px] mt-[9px] h-[32px] w-[93px] self-start'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
.appIcon {
|
||||
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
|
||||
.appIcon.small {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
|
||||
.appIcon.tiny {
|
||||
@apply w-6 h-6 text-base;
|
||||
}
|
||||
|
||||
.appIcon.xs {
|
||||
@apply w-5 h-5 text-base;
|
||||
}
|
||||
|
||||
.appIcon.rounded {
|
||||
@apply rounded-full;
|
||||
}
|
||||
@ -90,8 +90,4 @@ export const defaultPlan = {
|
||||
apiRateLimit: ALL_PLANS.sandbox.apiRateLimit,
|
||||
triggerEvents: ALL_PLANS.sandbox.triggerEvents,
|
||||
},
|
||||
reset: {
|
||||
apiRateLimit: null,
|
||||
triggerEvents: null,
|
||||
},
|
||||
}
|
||||
|
||||
@ -6,16 +6,15 @@ import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
RiBook2Line,
|
||||
RiFileEditLine,
|
||||
RiFlashlightLine,
|
||||
RiGraduationCapLine,
|
||||
RiGroupLine,
|
||||
RiSpeedLine,
|
||||
} from '@remixicon/react'
|
||||
import { Plan, SelfHostedPlan } from '../type'
|
||||
import { NUM_INFINITE } from '../config'
|
||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||
import VectorSpaceInfo from '../usage-info/vector-space-info'
|
||||
import AppsInfo from '../usage-info/apps-info'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -45,20 +44,9 @@ const PlanComp: FC<Props> = ({
|
||||
const {
|
||||
usage,
|
||||
total,
|
||||
reset,
|
||||
} = plan
|
||||
const triggerEventsResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
|
||||
? reset.triggerEvents ?? undefined
|
||||
: undefined
|
||||
const apiRateLimitResetInDays = (() => {
|
||||
if (total.apiRateLimit === NUM_INFINITE)
|
||||
return undefined
|
||||
if (typeof reset.apiRateLimit === 'number')
|
||||
return reset.apiRateLimit
|
||||
if (type === Plan.sandbox)
|
||||
return getDaysUntilEndOfMonth()
|
||||
return undefined
|
||||
})()
|
||||
const perMonthUnit = ` ${t('billing.usagePage.perMonth')}`
|
||||
const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit
|
||||
|
||||
const [showModal, setShowModal] = React.useState(false)
|
||||
const { mutateAsync } = useEducationVerify()
|
||||
@ -91,6 +79,7 @@ const PlanComp: FC<Props> = ({
|
||||
<div className='grow'>
|
||||
<div className='mb-1 flex items-center gap-1'>
|
||||
<div className='system-md-semibold-uppercase text-text-primary'>{t(`billing.plans.${type}.name`)}</div>
|
||||
<div className='system-2xs-medium-uppercase rounded-[5px] border border-divider-deep px-1 py-0.5 text-text-tertiary'>{t('billing.currentPlan')}</div>
|
||||
</div>
|
||||
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
|
||||
</div>
|
||||
@ -135,20 +124,18 @@ const PlanComp: FC<Props> = ({
|
||||
total={total.annotatedResponse}
|
||||
/>
|
||||
<UsageInfo
|
||||
Icon={TriggerAll}
|
||||
Icon={RiFlashlightLine}
|
||||
name={t('billing.usagePage.triggerEvents')}
|
||||
usage={usage.triggerEvents}
|
||||
total={total.triggerEvents}
|
||||
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
|
||||
resetInDays={triggerEventsResetInDays}
|
||||
unit={triggerEventUnit}
|
||||
/>
|
||||
<UsageInfo
|
||||
Icon={ApiAggregate}
|
||||
Icon={RiSpeedLine}
|
||||
name={t('billing.plansCommon.apiRateLimit')}
|
||||
usage={usage.apiRateLimit}
|
||||
total={total.apiRateLimit}
|
||||
tooltip={total.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
|
||||
resetInDays={apiRateLimitResetInDays}
|
||||
unit={perMonthUnit}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@ -46,10 +46,16 @@ const List = ({
|
||||
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
|
||||
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
|
||||
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
|
||||
}
|
||||
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
|
||||
/>
|
||||
<Item
|
||||
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
|
||||
/>
|
||||
<Divider bgStyle='gradient' />
|
||||
<Item
|
||||
label={
|
||||
planInfo.triggerEvents === NUM_INFINITE
|
||||
@ -58,14 +64,6 @@ const List = ({
|
||||
? t('billing.plansCommon.triggerEvents.sandbox', { count: planInfo.triggerEvents })
|
||||
: t('billing.plansCommon.triggerEvents.professional', { count: planInfo.triggerEvents })
|
||||
}
|
||||
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
plan === Plan.sandbox
|
||||
? t('billing.plansCommon.startNodes.limited', { count: 2 })
|
||||
: t('billing.plansCommon.startNodes.unlimited')
|
||||
}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
@ -75,7 +73,13 @@ const List = ({
|
||||
? t('billing.plansCommon.workflowExecution.faster')
|
||||
: t('billing.plansCommon.workflowExecution.priority')
|
||||
}
|
||||
tooltip={t('billing.plansCommon.workflowExecution.tooltip') as string}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
plan === Plan.sandbox
|
||||
? t('billing.plansCommon.startNodes.limited', { count: 2 })
|
||||
: t('billing.plansCommon.startNodes.unlimited')
|
||||
}
|
||||
/>
|
||||
<Divider bgStyle='gradient' />
|
||||
<Item
|
||||
@ -85,14 +89,6 @@ const List = ({
|
||||
<Item
|
||||
label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
planInfo.apiRateLimit === NUM_INFINITE
|
||||
? t('billing.plansCommon.unlimitedApiRate')
|
||||
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')} / ${t('billing.plansCommon.month')}`
|
||||
}
|
||||
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
|
||||
/>
|
||||
<Divider bgStyle='gradient' />
|
||||
<Item
|
||||
label={t('billing.plansCommon.modelProviders')}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
.surface {
|
||||
border: 0.5px solid var(--color-components-panel-border, rgba(16, 24, 40, 0.08));
|
||||
background:
|
||||
linear-gradient(109deg, var(--color-background-section, #f9fafb) 0%, var(--color-background-section-burn, #f2f4f7) 100%),
|
||||
var(--color-components-panel-bg, #fff);
|
||||
}
|
||||
|
||||
.heroOverlay {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='54' height='54' fill='none'%3E%3Crect x='1' y='1' width='48' height='48' rx='12' stroke='rgba(16, 24, 40, 0.3)' stroke-width='1' opacity='0.08'/%3E%3C/svg%3E");
|
||||
background-size: 54px 54px;
|
||||
background-position: 31px -23px;
|
||||
background-repeat: repeat;
|
||||
mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%);
|
||||
-webkit-mask-image: linear-gradient(180deg, rgba(255, 255, 255, 1) 45%, rgba(255, 255, 255, 0) 75%);
|
||||
}
|
||||
|
||||
.icon {
|
||||
border: 0.5px solid transparent;
|
||||
background:
|
||||
linear-gradient(180deg, var(--color-components-avatar-bg-mask-stop-0, rgba(255, 255, 255, 0.12)) 0%, var(--color-components-avatar-bg-mask-stop-100, rgba(255, 255, 255, 0.08)) 100%),
|
||||
var(--color-util-colors-blue-brand-blue-brand-500, #296dff);
|
||||
box-shadow: 0 10px 20px color-mix(in srgb, var(--color-util-colors-blue-brand-blue-brand-500, #296dff) 35%, transparent);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: linear-gradient(97deg, var(--color-components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -4%, var(--color-components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import i18next from 'i18next'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import TriggerEventsLimitModal from '.'
|
||||
import { Plan } from '../type'
|
||||
|
||||
const i18n = i18next.createInstance()
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
billing: {
|
||||
triggerLimitModal: {
|
||||
title: 'Upgrade to unlock more trigger events',
|
||||
description: 'You’ve reached the limit of workflow event triggers for this plan.',
|
||||
dismiss: 'Dismiss',
|
||||
upgrade: 'Upgrade',
|
||||
usageTitle: 'TRIGGER EVENTS',
|
||||
},
|
||||
usagePage: {
|
||||
triggerEvents: 'Trigger Events',
|
||||
resetsIn: 'Resets in {{count, number}} days',
|
||||
},
|
||||
upgradeBtn: {
|
||||
encourage: 'Upgrade Now',
|
||||
encourageShort: 'Upgrade',
|
||||
plain: 'View Plan',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Template = (args: React.ComponentProps<typeof TriggerEventsLimitModal>) => {
|
||||
const [visible, setVisible] = useState<boolean>(args.show ?? true)
|
||||
useEffect(() => {
|
||||
setVisible(args.show ?? true)
|
||||
}, [args.show])
|
||||
const handleHide = () => setVisible(false)
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<button
|
||||
className="rounded-lg border border-divider-subtle px-4 py-2 text-sm text-text-secondary hover:border-divider-deep hover:text-text-primary"
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
Open Modal
|
||||
</button>
|
||||
<TriggerEventsLimitModal
|
||||
{...args}
|
||||
show={visible}
|
||||
onDismiss={handleHide}
|
||||
onUpgrade={handleHide}
|
||||
/>
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Billing/TriggerEventsLimitModal',
|
||||
component: TriggerEventsLimitModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
show: true,
|
||||
usage: 120,
|
||||
total: 120,
|
||||
resetInDays: 5,
|
||||
planType: Plan.professional,
|
||||
},
|
||||
} satisfies Meta<typeof TriggerEventsLimitModal>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Professional: Story = {
|
||||
args: {
|
||||
onDismiss: () => { /* noop */ },
|
||||
onUpgrade: () => { /* noop */ },
|
||||
},
|
||||
render: args => <Template {...args} />,
|
||||
}
|
||||
|
||||
export const Sandbox: Story = {
|
||||
render: args => <Template {...args} />,
|
||||
args: {
|
||||
onDismiss: () => { /* noop */ },
|
||||
onUpgrade: () => { /* noop */ },
|
||||
resetInDays: undefined,
|
||||
planType: Plan.sandbox,
|
||||
},
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import UsageInfo from '@/app/components/billing/usage-info'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import type { Plan } from '@/app/components/billing/type'
|
||||
import styles from './index.module.css'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
onDismiss: () => void
|
||||
onUpgrade: () => void
|
||||
usage: number
|
||||
total: number
|
||||
resetInDays?: number
|
||||
planType: Plan
|
||||
}
|
||||
|
||||
const TriggerEventsLimitModal: FC<Props> = ({
|
||||
show,
|
||||
onDismiss,
|
||||
onUpgrade,
|
||||
usage,
|
||||
total,
|
||||
resetInDays,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onDismiss}
|
||||
closable={false}
|
||||
clickOutsideNotClose
|
||||
className={`${styles.surface} flex h-[360px] w-[580px] flex-col overflow-hidden rounded-2xl !p-0 shadow-xl`}
|
||||
>
|
||||
<div className='relative flex w-full flex-1 items-stretch justify-center'>
|
||||
<div
|
||||
aria-hidden
|
||||
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
|
||||
/>
|
||||
<div className='relative z-10 flex w-full flex-col items-start gap-4 px-8 pt-8'>
|
||||
<div className={`${styles.icon} flex h-12 w-12 items-center justify-center rounded-[12px]`}>
|
||||
<TriggerAll className='h-5 w-5 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<div className={`${styles.highlight} title-lg-semi-bold`}>
|
||||
{t('billing.triggerLimitModal.title')}
|
||||
</div>
|
||||
<div className='body-md-regular text-text-secondary'>
|
||||
{t('billing.triggerLimitModal.description')}
|
||||
</div>
|
||||
</div>
|
||||
<UsageInfo
|
||||
className='mb-5 w-full rounded-[12px] bg-components-panel-on-panel-item-bg'
|
||||
Icon={TriggerAll}
|
||||
name={t('billing.triggerLimitModal.usageTitle')}
|
||||
usage={usage}
|
||||
total={total}
|
||||
resetInDays={resetInDays}
|
||||
hideIcon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[76px] w-full items-center justify-end gap-2 px-8 pb-8 pt-5'>
|
||||
<Button
|
||||
className='h-8 w-[77px] min-w-[72px] !rounded-lg !border-[0.5px] px-3 py-2'
|
||||
onClick={onDismiss}
|
||||
>
|
||||
{t('billing.triggerLimitModal.dismiss')}
|
||||
</Button>
|
||||
<UpgradeBtn
|
||||
isShort
|
||||
onClick={onUpgrade}
|
||||
className='flex w-[93px] items-center justify-center !rounded-lg !px-2'
|
||||
style={{ height: 32 }}
|
||||
labelKey='billing.triggerLimitModal.upgrade'
|
||||
loc='trigger-events-limit-modal'
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TriggerEventsLimitModal)
|
||||
@ -55,11 +55,6 @@ export type SelfHostedPlanInfo = {
|
||||
|
||||
export type UsagePlanInfo = Pick<PlanInfo, 'buildApps' | 'teamMembers' | 'annotatedResponse' | 'documentsUploadQuota' | 'apiRateLimit' | 'triggerEvents'> & { vectorSpace: number }
|
||||
|
||||
export type UsageResetInfo = {
|
||||
apiRateLimit?: number | null
|
||||
triggerEvents?: number | null
|
||||
}
|
||||
|
||||
export enum DocumentProcessingPriority {
|
||||
standard = 'standard',
|
||||
priority = 'priority',
|
||||
@ -96,12 +91,10 @@ export type CurrentPlanInfoBackend = {
|
||||
api_rate_limit?: {
|
||||
size: number
|
||||
limit: number // total. 0 means unlimited
|
||||
reset_in_days?: number
|
||||
}
|
||||
trigger_events?: {
|
||||
size: number
|
||||
limit: number // total. 0 means unlimited
|
||||
reset_in_days?: number
|
||||
}
|
||||
docs_processing: DocumentProcessingPriority
|
||||
can_replace_logo: boolean
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '../../base/premium-badge'
|
||||
@ -9,24 +9,19 @@ import { useModalContext } from '@/context/modal-context'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
isFull?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
isPlain?: boolean
|
||||
isShort?: boolean
|
||||
onClick?: () => void
|
||||
loc?: string
|
||||
labelKey?: string
|
||||
}
|
||||
|
||||
const UpgradeBtn: FC<Props> = ({
|
||||
className,
|
||||
style,
|
||||
isPlain = false,
|
||||
isShort = false,
|
||||
onClick: _onClick,
|
||||
loc,
|
||||
labelKey,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
@ -45,17 +40,10 @@ const UpgradeBtn: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)
|
||||
const label = labelKey ? t(labelKey) : defaultBadgeLabel
|
||||
|
||||
if (isPlain) {
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{labelKey ? label : t('billing.upgradeBtn.plain')}
|
||||
<Button onClick={onClick}>
|
||||
{t('billing.upgradeBtn.plain')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@ -66,13 +54,11 @@ const UpgradeBtn: FC<Props> = ({
|
||||
color='blue'
|
||||
allowHover={true}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<SparklesSoft className='flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0' />
|
||||
<div className='system-xs-medium'>
|
||||
<span className='p-1'>
|
||||
{label}
|
||||
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
|
||||
@ -16,12 +16,10 @@ type Props = {
|
||||
total: number
|
||||
unit?: string
|
||||
unitPosition?: 'inline' | 'suffix'
|
||||
resetHint?: string
|
||||
resetInDays?: number
|
||||
hideIcon?: boolean
|
||||
}
|
||||
|
||||
const WARNING_THRESHOLD = 80
|
||||
const LOW = 50
|
||||
const MIDDLE = 80
|
||||
|
||||
const UsageInfo: FC<Props> = ({
|
||||
className,
|
||||
@ -32,39 +30,28 @@ const UsageInfo: FC<Props> = ({
|
||||
total,
|
||||
unit,
|
||||
unitPosition = 'suffix',
|
||||
resetHint,
|
||||
resetInDays,
|
||||
hideIcon = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const percent = usage / total * 100
|
||||
const color = percent >= 100
|
||||
? 'bg-components-progress-error-progress'
|
||||
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
|
||||
const color = (() => {
|
||||
if (percent < LOW)
|
||||
return 'bg-components-progress-bar-progress-solid'
|
||||
|
||||
if (percent < MIDDLE)
|
||||
return 'bg-components-progress-warning-progress'
|
||||
|
||||
return 'bg-components-progress-error-progress'
|
||||
})()
|
||||
const isUnlimited = total === NUM_INFINITE
|
||||
let totalDisplay: string | number = isUnlimited ? t('billing.plansCommon.unlimited') : total
|
||||
if (!isUnlimited && unit && unitPosition === 'inline')
|
||||
totalDisplay = `${total}${unit}`
|
||||
const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix'
|
||||
const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('billing.usagePage.resetsIn', { count: resetInDays }) : undefined)
|
||||
const rightInfo = resetText
|
||||
? (
|
||||
<div className='system-xs-regular ml-auto flex-1 text-right text-text-tertiary'>
|
||||
{resetText}
|
||||
</div>
|
||||
)
|
||||
: (showUnit && (
|
||||
<div className='system-xs-medium ml-auto text-text-tertiary'>
|
||||
{unit}
|
||||
</div>
|
||||
))
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-2 rounded-xl bg-components-panel-bg p-4', className)}>
|
||||
{!hideIcon && Icon && (
|
||||
<Icon className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
<Icon className='h-4 w-4 text-text-tertiary' />
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='system-xs-medium text-text-tertiary'>{name}</div>
|
||||
{tooltip && (
|
||||
@ -83,7 +70,11 @@ const UsageInfo: FC<Props> = ({
|
||||
<div className='system-md-regular text-text-quaternary'>/</div>
|
||||
<div>{totalDisplay}</div>
|
||||
</div>
|
||||
{rightInfo}
|
||||
{showUnit && (
|
||||
<div className='system-xs-medium ml-auto text-text-tertiary'>
|
||||
{unit}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProgressBar
|
||||
percent={percent}
|
||||
|
||||
@ -36,9 +36,5 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
|
||||
apiRateLimit: resolveLimit(data.api_rate_limit?.limit, planPreset?.apiRateLimit ?? NUM_INFINITE),
|
||||
triggerEvents: resolveLimit(data.trigger_events?.limit, planPreset?.triggerEvents),
|
||||
},
|
||||
reset: {
|
||||
apiRateLimit: data.api_rate_limit?.reset_in_days ?? null,
|
||||
triggerEvents: data.trigger_events?.reset_in_days ?? null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,8 +24,8 @@ import { debounce } from 'lodash-es'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LogViewer from '../log-viewer'
|
||||
import { usePluginSubscriptionStore } from '../store'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { useSubscriptionList } from '../use-subscription-list'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@ -91,7 +91,7 @@ const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
|
||||
export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
const { refetch } = useSubscriptionList()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
|
||||
|
||||
@ -295,7 +295,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
message: t('pluginTrigger.subscription.createSuccess'),
|
||||
})
|
||||
onClose()
|
||||
refresh?.()
|
||||
refetch?.()
|
||||
},
|
||||
onError: async (error: any) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed')
|
||||
|
||||
@ -4,7 +4,7 @@ import Toast from '@/app/components/base/toast'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginSubscriptionStore } from './store'
|
||||
import { useSubscriptionList } from './use-subscription-list'
|
||||
|
||||
type Props = {
|
||||
onClose: (deleted: boolean) => void
|
||||
@ -18,7 +18,7 @@ const tPrefix = 'pluginTrigger.subscription.list.item.actions.deleteConfirm'
|
||||
|
||||
export const DeleteConfirm = (props: Props) => {
|
||||
const { onClose, isShow, currentId, currentName, workflowsInUse } = props
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
const { refetch } = useSubscriptionList()
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
const { t } = useTranslation()
|
||||
const [inputName, setInputName] = useState('')
|
||||
@ -40,7 +40,7 @@ export const DeleteConfirm = (props: Props) => {
|
||||
message: t(`${tPrefix}.success`, { name: currentName }),
|
||||
className: 'z-[10000001]',
|
||||
})
|
||||
refresh?.()
|
||||
refetch?.()
|
||||
onClose(true)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
type ShapeSubscription = {
|
||||
refresh?: () => void
|
||||
setRefresh: (refresh: () => void) => void
|
||||
}
|
||||
|
||||
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
|
||||
refresh: undefined,
|
||||
setRefresh: (refresh: () => void) => set({ refresh }),
|
||||
}))
|
||||
@ -1,19 +1,11 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import { usePluginStore } from '../store'
|
||||
import { usePluginSubscriptionStore } from './store'
|
||||
|
||||
export const useSubscriptionList = () => {
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { setRefresh } = usePluginSubscriptionStore()
|
||||
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(detail?.provider || '')
|
||||
|
||||
useEffect(() => {
|
||||
if (refetch)
|
||||
setRefresh(refetch)
|
||||
}, [refetch, setRefresh])
|
||||
|
||||
return {
|
||||
detail,
|
||||
subscriptions,
|
||||
|
||||
@ -40,8 +40,6 @@ import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useIsChatMode } from '@/app/components/workflow/hooks'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -52,7 +50,6 @@ const FeaturesTrigger = () => {
|
||||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
@ -98,15 +95,6 @@ const FeaturesTrigger = () => {
|
||||
const hasTriggerNode = useMemo(() => (
|
||||
nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
|
||||
), [nodes])
|
||||
const startNodeLimitExceeded = useMemo(() => {
|
||||
const entryCount = nodes.reduce((count, node) => {
|
||||
const nodeType = node.data.type as BlockEnum
|
||||
if (nodeType === BlockEnum.Start || isTriggerNode(nodeType))
|
||||
return count + 1
|
||||
return count
|
||||
}, 0)
|
||||
return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2
|
||||
}, [nodes, plan.type, isFetchedPlan])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
@ -208,8 +196,7 @@ const FeaturesTrigger = () => {
|
||||
crossAxisOffset: 4,
|
||||
missingStartNode: !startNode,
|
||||
hasTriggerNode,
|
||||
startNodeLimitExceeded,
|
||||
publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded,
|
||||
publishDisabled: !hasWorkflowNodes,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -298,7 +298,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTriggerPlugin?.subscription_constructor) {
|
||||
if (currentTriggerPlugin) {
|
||||
setDetail({
|
||||
name: currentTriggerPlugin.label[language],
|
||||
plugin_id: currentTriggerPlugin.plugin_id || '',
|
||||
|
||||
@ -1,130 +0,0 @@
|
||||
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import type { ModalState } from '../modal-context'
|
||||
|
||||
export type TriggerEventsLimitModalPayload = {
|
||||
usage: number
|
||||
total: number
|
||||
resetInDays?: number
|
||||
planType: Plan
|
||||
storageKey?: string
|
||||
persistDismiss?: boolean
|
||||
}
|
||||
|
||||
type TriggerPlanInfo = {
|
||||
type: Plan
|
||||
usage: { triggerEvents: number }
|
||||
total: { triggerEvents: number }
|
||||
reset: { triggerEvents?: number | null }
|
||||
}
|
||||
|
||||
type UseTriggerEventsLimitModalOptions = {
|
||||
plan: TriggerPlanInfo
|
||||
isFetchedPlan: boolean
|
||||
currentWorkspaceId?: string
|
||||
}
|
||||
|
||||
type UseTriggerEventsLimitModalResult = {
|
||||
showTriggerEventsLimitModal: ModalState<TriggerEventsLimitModalPayload> | null
|
||||
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
|
||||
persistTriggerEventsLimitModalDismiss: () => void
|
||||
}
|
||||
|
||||
const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed'
|
||||
|
||||
export const useTriggerEventsLimitModal = ({
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
currentWorkspaceId,
|
||||
}: UseTriggerEventsLimitModalOptions): UseTriggerEventsLimitModalResult => {
|
||||
const [showTriggerEventsLimitModal, setShowTriggerEventsLimitModal] = useState<ModalState<TriggerEventsLimitModalPayload> | null>(null)
|
||||
const dismissedTriggerEventsLimitStorageKeysRef = useRef<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_CLOUD_EDITION)
|
||||
return
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
if (!currentWorkspaceId)
|
||||
return
|
||||
if (!isFetchedPlan) {
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { type, usage, total, reset } = plan
|
||||
const isUnlimited = total.triggerEvents === NUM_INFINITE
|
||||
const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents
|
||||
|
||||
if (type === Plan.team || isUnlimited || !reachedLimit) {
|
||||
if (showTriggerEventsLimitModal)
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
const triggerResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
|
||||
? reset.triggerEvents ?? undefined
|
||||
: undefined
|
||||
const cycleTag = (() => {
|
||||
if (typeof reset.triggerEvents === 'number')
|
||||
return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD')
|
||||
if (type === Plan.sandbox)
|
||||
return dayjs().endOf('month').format('YYYY-MM-DD')
|
||||
return 'none'
|
||||
})()
|
||||
const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}`
|
||||
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
|
||||
return
|
||||
|
||||
let persistDismiss = true
|
||||
let hasDismissed = false
|
||||
try {
|
||||
if (localStorage.getItem(storageKey) === '1')
|
||||
hasDismissed = true
|
||||
}
|
||||
catch {
|
||||
persistDismiss = false
|
||||
}
|
||||
if (hasDismissed)
|
||||
return
|
||||
|
||||
if (showTriggerEventsLimitModal?.payload.storageKey === storageKey)
|
||||
return
|
||||
|
||||
setShowTriggerEventsLimitModal({
|
||||
payload: {
|
||||
usage: usage.triggerEvents,
|
||||
total: total.triggerEvents,
|
||||
planType: type,
|
||||
resetInDays: triggerResetInDays,
|
||||
storageKey,
|
||||
persistDismiss,
|
||||
},
|
||||
})
|
||||
}, [plan, isFetchedPlan, showTriggerEventsLimitModal, currentWorkspaceId])
|
||||
|
||||
const persistTriggerEventsLimitModalDismiss = useCallback(() => {
|
||||
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
||||
if (!storageKey)
|
||||
return
|
||||
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, '1')
|
||||
return
|
||||
}
|
||||
catch {
|
||||
// ignore error and fall back to in-memory guard
|
||||
}
|
||||
}
|
||||
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
|
||||
}, [showTriggerEventsLimitModal])
|
||||
|
||||
return {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
}
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
import React from 'react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
|
||||
jest.mock('@/config', () => {
|
||||
const actual = jest.requireActual('@/config')
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useSearchParams: jest.fn(() => new URLSearchParams()),
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = jest.fn()
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
}))
|
||||
|
||||
const mockUseAppContext = jest.fn()
|
||||
jest.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => mockUseAppContext(),
|
||||
}))
|
||||
|
||||
let latestTriggerEventsModalProps: any = null
|
||||
const triggerEventsLimitModalMock = jest.fn((props: any) => {
|
||||
latestTriggerEventsModalProps = props
|
||||
return (
|
||||
<div data-testid="trigger-limit-modal">
|
||||
<button type="button" onClick={props.onDismiss}>dismiss</button>
|
||||
<button type="button" onClick={props.onUpgrade}>upgrade</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => triggerEventsLimitModalMock(props),
|
||||
}))
|
||||
|
||||
type DefaultPlanShape = typeof defaultPlan
|
||||
type PlanOverrides = Partial<Omit<DefaultPlanShape, 'usage' | 'total' | 'reset'>> & {
|
||||
usage?: Partial<DefaultPlanShape['usage']>
|
||||
total?: Partial<DefaultPlanShape['total']>
|
||||
reset?: Partial<DefaultPlanShape['reset']>
|
||||
}
|
||||
|
||||
const createPlan = (overrides: PlanOverrides = {}): DefaultPlanShape => ({
|
||||
...defaultPlan,
|
||||
...overrides,
|
||||
usage: {
|
||||
...defaultPlan.usage,
|
||||
...(overrides.usage ?? {}),
|
||||
},
|
||||
total: {
|
||||
...defaultPlan.total,
|
||||
...(overrides.total ?? {}),
|
||||
},
|
||||
reset: {
|
||||
...defaultPlan.reset,
|
||||
...(overrides.reset ?? {}),
|
||||
},
|
||||
})
|
||||
|
||||
const renderProvider = () => render(
|
||||
<ModalContextProvider>
|
||||
<div data-testid="modal-context-test-child" />
|
||||
</ModalContextProvider>,
|
||||
)
|
||||
|
||||
describe('ModalContextProvider trigger events limit modal', () => {
|
||||
beforeEach(() => {
|
||||
latestTriggerEventsModalProps = null
|
||||
triggerEventsLimitModalMock.mockClear()
|
||||
mockUseAppContext.mockReset()
|
||||
mockUseProviderContext.mockReset()
|
||||
window.localStorage.clear()
|
||||
mockUseAppContext.mockReturnValue({
|
||||
currentWorkspace: {
|
||||
id: 'workspace-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 3000 },
|
||||
total: { triggerEvents: 3000 },
|
||||
reset: { triggerEvents: 5 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
expect(latestTriggerEventsModalProps).toMatchObject({
|
||||
usage: 3000,
|
||||
total: 3000,
|
||||
resetInDays: 5,
|
||||
planType: Plan.professional,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
const [key, value] = setItemSpy.mock.calls[0]
|
||||
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
||||
expect(value).toBe('1')
|
||||
})
|
||||
|
||||
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 200 },
|
||||
total: { triggerEvents: 200 },
|
||||
reset: { triggerEvents: 3 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
||||
throw new Error('Storage disabled')
|
||||
})
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
expect(setItemSpy).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 120 },
|
||||
total: { triggerEvents: 120 },
|
||||
reset: { triggerEvents: 2 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
||||
throw new Error('Quota exceeded')
|
||||
})
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
})
|
||||
@ -36,12 +36,6 @@ import { noop } from 'lodash-es'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
|
||||
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
type TriggerEventsLimitModalPayload,
|
||||
useTriggerEventsLimitModal,
|
||||
} from './hooks/use-trigger-events-limit-modal'
|
||||
|
||||
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
|
||||
ssr: false,
|
||||
@ -80,9 +74,6 @@ const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugi
|
||||
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export type ModalState<T> = {
|
||||
payload: T
|
||||
@ -122,7 +113,6 @@ export type ModalContextState = {
|
||||
}> | null>>
|
||||
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
|
||||
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
|
||||
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
|
||||
}
|
||||
const PRICING_MODAL_QUERY_PARAM = 'pricing'
|
||||
const PRICING_MODAL_QUERY_VALUE = 'open'
|
||||
@ -140,7 +130,6 @@ const ModalContext = createContext<ModalContextState>({
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
setShowEducationExpireNoticeModal: noop,
|
||||
setShowTriggerEventsLimitModal: noop,
|
||||
})
|
||||
|
||||
export const useModalContext = () => useContext(ModalContext)
|
||||
@ -179,7 +168,6 @@ export const ModalContextProvider = ({
|
||||
}> | null>(null)
|
||||
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
|
||||
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
||||
const { currentWorkspace } = useAppContext()
|
||||
|
||||
const [showPricingModal, setShowPricingModal] = useState(
|
||||
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
|
||||
@ -240,17 +228,6 @@ export const ModalContextProvider = ({
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
}, [showPricingModal])
|
||||
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
} = useTriggerEventsLimitModal({
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
currentWorkspaceId: currentWorkspace?.id,
|
||||
})
|
||||
|
||||
const handleCancelModerationSettingModal = () => {
|
||||
setShowModerationSettingModal(null)
|
||||
if (showModerationSettingModal?.onCancelCallback)
|
||||
@ -357,7 +334,6 @@ export const ModalContextProvider = ({
|
||||
setShowOpeningModal,
|
||||
setShowUpdatePluginModal,
|
||||
setShowEducationExpireNoticeModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
}}>
|
||||
<>
|
||||
{children}
|
||||
@ -479,25 +455,6 @@ export const ModalContextProvider = ({
|
||||
onClose={() => setShowEducationExpireNoticeModal(null)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!!showTriggerEventsLimitModal && (
|
||||
<TriggerEventsLimitModal
|
||||
show
|
||||
usage={showTriggerEventsLimitModal.payload.usage}
|
||||
total={showTriggerEventsLimitModal.payload.total}
|
||||
planType={showTriggerEventsLimitModal.payload.planType}
|
||||
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
|
||||
onDismiss={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
}}
|
||||
onUpgrade={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
handleShowPricingModal()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ModalContext.Provider>
|
||||
)
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import type { Plan, UsageResetInfo } from '@/app/components/billing/type'
|
||||
import type { Plan } from '@/app/components/billing/type'
|
||||
import type { UsagePlanInfo } from '@/app/components/billing/type'
|
||||
import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||
import { parseCurrentPlan } from '@/app/components/billing/utils'
|
||||
@ -40,7 +40,6 @@ type ProviderContextState = {
|
||||
type: Plan
|
||||
usage: UsagePlanInfo
|
||||
total: UsagePlanInfo
|
||||
reset: UsageResetInfo
|
||||
}
|
||||
isFetchedPlan: boolean
|
||||
enableBilling: boolean
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Abschießen',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'Backend-Service-API',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'In Betrieb',
|
||||
disable: 'Deaktivieren',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Die Funktion {{feature}} wird im Trigger-Knoten-Modus nicht unterstützt.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analyse',
|
||||
|
||||
@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Nachrichtenguthaben',
|
||||
tooltip: 'Nachrichtenaufrufkontingente für verschiedene Tarife unter Verwendung von OpenAI-Modellen (außer gpt4).Nachrichten über dem Limit verwenden Ihren OpenAI-API-Schlüssel.',
|
||||
titlePerMonth: '{{count,number}} Nachrichten / Monat',
|
||||
titlePerMonth: '{{count,number}} Nachrichten/Monat',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Kontingentgrenzen für Annotationen',
|
||||
@ -83,7 +83,7 @@ const translation = {
|
||||
cloud: 'Cloud-Dienst',
|
||||
apiRateLimitTooltip: 'Die API-Datenbeschränkung gilt für alle Anfragen, die über die Dify-API gemacht werden, einschließlich Textgenerierung, Chat-Konversationen, Workflow-Ausführungen und Dokumentenverarbeitung.',
|
||||
getStarted: 'Loslegen',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/Monat',
|
||||
documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.',
|
||||
apiRateLimit: 'API-Datenlimit',
|
||||
documents: '{{count,number}} Wissensdokumente',
|
||||
|
||||
@ -9,16 +9,8 @@ const translation = {
|
||||
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
|
||||
triggerEvents: 'Trigger Events',
|
||||
perMonth: 'per month',
|
||||
resetsIn: 'Resets in {{count,number}} days',
|
||||
},
|
||||
teamMembers: 'Team Members',
|
||||
triggerLimitModal: {
|
||||
title: 'Upgrade to unlock more trigger events',
|
||||
description: 'You’ve reached the limit of workflow event triggers for this plan.',
|
||||
dismiss: 'Dismiss',
|
||||
upgrade: 'Upgrade',
|
||||
usageTitle: 'TRIGGER EVENTS',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: 'View Plan',
|
||||
encourage: 'Upgrade Now',
|
||||
@ -69,11 +61,11 @@ const translation = {
|
||||
documentsTooltip: 'Quota on the number of documents imported from the Knowledge Data Source.',
|
||||
vectorSpace: '{{size}} Knowledge Data Storage',
|
||||
vectorSpaceTooltip: 'Documents with the High Quality indexing mode will consume Knowledge Data Storage resources. When Knowledge Data Storage reaches the limit, new documents will not be uploaded.',
|
||||
documentsRequestQuota: '{{count,number}} Knowledge Request / min',
|
||||
documentsRequestQuota: '{{count,number}}/min Knowledge Request Rate Limit',
|
||||
documentsRequestQuotaTooltip: 'Specifies the total number of actions a workspace can perform per minute within the knowledge base, including dataset creation, deletion, updates, document uploads, modifications, archiving, and knowledge base queries. This metric is used to evaluate the performance of knowledge base requests. For example, if a Sandbox user performs 10 consecutive hit tests within one minute, their workspace will be temporarily restricted from performing the following actions for the next minute: dataset creation, deletion, updates, and document uploads or modifications. ',
|
||||
apiRateLimit: 'API Rate Limit',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
unlimitedApiRate: 'No Dify API Rate Limit',
|
||||
apiRateLimitUnit: '{{count,number}}/month',
|
||||
unlimitedApiRate: 'No API Rate Limit',
|
||||
apiRateLimitTooltip: 'API Rate Limit applies to all requests made through the Dify API, including text generation, chat conversations, workflow executions, and document processing.',
|
||||
documentProcessingPriority: ' Document Processing',
|
||||
documentProcessingPriorityUpgrade: 'Process more data with higher accuracy at faster speeds.',
|
||||
@ -84,19 +76,17 @@ const translation = {
|
||||
},
|
||||
triggerEvents: {
|
||||
sandbox: '{{count,number}} Trigger Events',
|
||||
professional: '{{count,number}} Trigger Events / month',
|
||||
professional: '{{count,number}} Trigger Events/month',
|
||||
unlimited: 'Unlimited Trigger Events',
|
||||
tooltip: 'The number of events that automatically start workflows through Plugin, Schedule, or Webhook triggers.',
|
||||
},
|
||||
workflowExecution: {
|
||||
standard: 'Standard Workflow Execution',
|
||||
faster: 'Faster Workflow Execution',
|
||||
priority: 'Priority Workflow Execution',
|
||||
tooltip: 'Workflow execution queue priority and speed.',
|
||||
},
|
||||
startNodes: {
|
||||
limited: 'Up to {{count}} Start Nodes / workflow',
|
||||
unlimited: 'Unlimited Start Nodes / workflow',
|
||||
limited: 'Up to {{count}} Start Nodes per Workflow',
|
||||
unlimited: 'Unlimited Start Nodes per Workflow',
|
||||
},
|
||||
logsHistory: '{{days}} Log history',
|
||||
customTools: 'Custom Tools',
|
||||
@ -125,7 +115,7 @@ const translation = {
|
||||
memberAfter: 'Member',
|
||||
messageRequest: {
|
||||
title: '{{count,number}} message credits',
|
||||
titlePerMonth: '{{count,number}} message credits / month',
|
||||
titlePerMonth: '{{count,number}} message credits/month',
|
||||
tooltip: 'Message credits are provided to help you easily try out different OpenAI models in Dify. Credits are consumed based on the model type. Once they’re used up, you can switch to your own OpenAI API key.',
|
||||
},
|
||||
annotatedResponse: {
|
||||
|
||||
@ -123,11 +123,6 @@ const translation = {
|
||||
noHistory: 'No History',
|
||||
tagBound: 'Number of apps using this tag',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: 'Upgrade to',
|
||||
startNodeTitleSuffix: 'unlock unlimited start nodes',
|
||||
startNodeDesc: 'You’ve reached the limit of 2 start nodes for your current plan. Upgrade to publish this workflow.',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Environment Variables',
|
||||
envDescription: 'Environment variables can be used to store private information and credentials. They are read-only and can be separated from the DSL file during export.',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Lanzar',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API del servicio backend',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'En servicio',
|
||||
disable: 'Deshabilitar',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'La función {{feature}} no es compatible en el modo Nodo de disparo.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Análisis',
|
||||
|
||||
@ -65,7 +65,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Créditos de Mensajes',
|
||||
tooltip: 'Cuotas de invocación de mensajes para varios planes utilizando modelos de OpenAI (excepto gpt4). Los mensajes que excedan el límite utilizarán tu clave API de OpenAI.',
|
||||
titlePerMonth: '{{count,number}} mensajes / mes',
|
||||
titlePerMonth: '{{count,number}} mensajes/mes',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Límites de Cuota de Anotación',
|
||||
@ -76,7 +76,7 @@ const translation = {
|
||||
priceTip: 'por espacio de trabajo/',
|
||||
teamMember_one: '{{count, número}} Miembro del Equipo',
|
||||
getStarted: 'Comenzar',
|
||||
apiRateLimitUnit: '{{count, número}}',
|
||||
apiRateLimitUnit: '{{count, número}}/mes',
|
||||
freeTrialTipSuffix: 'No se requiere tarjeta de crédito',
|
||||
unlimitedApiRate: 'Sin límite de tasa de API',
|
||||
apiRateLimit: 'Límite de tasa de API',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'راه اندازی',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API سرویس بکاند',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'در حال سرویسدهی',
|
||||
disable: 'غیرفعال',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'ویژگی {{feature}} در حالت گره تریگر پشتیبانی نمیشود.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'تحلیل',
|
||||
|
||||
@ -73,7 +73,7 @@ const translation = {
|
||||
},
|
||||
ragAPIRequestTooltip: 'به تعداد درخواستهای API که فقط قابلیتهای پردازش پایگاه دانش Dify را فراخوانی میکنند اشاره دارد.',
|
||||
receiptInfo: 'فقط صاحب تیم و مدیر تیم میتوانند اشتراک تهیه کنند و اطلاعات صورتحساب را مشاهده کنند',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/ماه',
|
||||
cloud: 'سرویس ابری',
|
||||
documents: '{{count,number}} سندهای دانش',
|
||||
self: 'خود میزبان',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Lancer',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API de service Backend',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'En service',
|
||||
disable: 'Désactiver',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'La fonctionnalité {{feature}} n\'est pas prise en charge en mode Nœud Déclencheur.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analyse',
|
||||
|
||||
@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Crédits de message',
|
||||
tooltip: 'Quotas d\'invocation de messages pour divers plans utilisant les modèles OpenAI (sauf gpt4). Les messages dépassant la limite utiliseront votre clé API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} messages / mois',
|
||||
titlePerMonth: '{{count,number}} messages/mois',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Limites de quota d\'annotation',
|
||||
@ -73,7 +73,7 @@ const translation = {
|
||||
ragAPIRequestTooltip: 'Fait référence au nombre d\'appels API invoquant uniquement les capacités de traitement de la base de connaissances de Dify.',
|
||||
receiptInfo: 'Seuls le propriétaire de l\'équipe et l\'administrateur de l\'équipe peuvent s\'abonner et consulter les informations de facturation',
|
||||
annotationQuota: 'Quota d’annotation',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/mois',
|
||||
priceTip: 'par espace de travail/',
|
||||
freeTrialTipSuffix: 'Aucune carte de crédit requise',
|
||||
teamWorkspace: '{{count,number}} Espace de travail d\'équipe',
|
||||
|
||||
@ -125,6 +125,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'लॉन्च',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'बैकएंड सेवा एपीआई',
|
||||
@ -136,6 +137,10 @@ const translation = {
|
||||
running: 'सेवा में',
|
||||
disable: 'अक्षम करें',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'ट्रिगर नोड मोड में {{feature}} फ़ीचर समर्थित नहीं है।',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'विश्लेषण',
|
||||
|
||||
@ -70,7 +70,7 @@ const translation = {
|
||||
title: 'संदेश क्रेडिट्स',
|
||||
tooltip:
|
||||
'विभिन्न योजनाओं के लिए संदेश आह्वान कोटा OpenAI मॉडलों का उपयोग करके (gpt4 को छोड़कर)। सीमा से अधिक संदेश आपके OpenAI API कुंजी का उपयोग करेंगे।',
|
||||
titlePerMonth: '{{count,number}} संदेश / महीना',
|
||||
titlePerMonth: '{{count,number}} संदेश/महीना',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'एनोटेशन कोटा सीमाएं',
|
||||
@ -96,7 +96,7 @@ const translation = {
|
||||
freeTrialTip: '200 ओपनएआई कॉल्स का मुफ्त परीक्षण।',
|
||||
documents: '{{count,number}} ज्ञान दस्तावेज़',
|
||||
freeTrialTipSuffix: 'कोई क्रेडिट कार्ड की आवश्यकता नहीं है',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/माह',
|
||||
teamWorkspace: '{{count,number}} टीम कार्यक्षेत्र',
|
||||
apiRateLimitTooltip: 'Dify API के माध्यम से की गई सभी अनुरोधों पर API दर सीमा लागू होती है, जिसमें टेक्स्ट जनरेशन, चैट वार्तालाप, कार्यप्रवाह निष्पादन और दस्तावेज़ प्रसंस्करण शामिल हैं।',
|
||||
teamMember_one: '{{count,number}} टीम सदस्य',
|
||||
|
||||
@ -111,6 +111,7 @@ const translation = {
|
||||
preUseReminder: 'Harap aktifkan aplikasi web sebelum melanjutkan.',
|
||||
regenerateNotice: 'Apakah Anda ingin membuat ulang URL publik?',
|
||||
explanation: 'Aplikasi web AI siap pakai',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
accessibleAddress: 'Titik Akhir API Layanan',
|
||||
@ -123,6 +124,10 @@ const translation = {
|
||||
running: 'Berjalan',
|
||||
},
|
||||
title: 'Ikhtisar',
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Fitur {{feature}} tidak didukung dalam mode Node Pemicu.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
totalMessages: {
|
||||
|
||||
@ -127,6 +127,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Lanciare',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API del servizio backend',
|
||||
@ -138,6 +139,10 @@ const translation = {
|
||||
running: 'In servizio',
|
||||
disable: 'Disabilita',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'La funzionalità {{feature}} non è supportata in modalità Nodo Trigger.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analisi',
|
||||
|
||||
@ -70,7 +70,7 @@ const translation = {
|
||||
title: 'Crediti Messaggi',
|
||||
tooltip:
|
||||
'Quote di invocazione dei messaggi per vari piani utilizzando i modelli OpenAI (eccetto gpt4). I messaggi oltre il limite utilizzeranno la tua chiave API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} messaggi / mese',
|
||||
titlePerMonth: '{{count,number}} messaggi/mese',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Limiti di Quota di Annotazione',
|
||||
@ -88,7 +88,7 @@ const translation = {
|
||||
freeTrialTipPrefix: 'Iscriviti e ricevi un',
|
||||
teamMember_one: '{{count,number}} membro del team',
|
||||
documents: '{{count,number}} Documenti di Conoscenza',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/mese',
|
||||
documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza',
|
||||
teamMember_other: '{{count,number}} membri del team',
|
||||
freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.',
|
||||
|
||||
@ -7,16 +7,8 @@ const translation = {
|
||||
documentsUploadQuota: 'ドキュメント・アップロード・クォータ',
|
||||
vectorSpace: 'ナレッジベースのデータストレージ',
|
||||
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
|
||||
triggerEvents: 'トリガーイベント数',
|
||||
triggerEvents: 'トリガーイベント',
|
||||
perMonth: '月あたり',
|
||||
resetsIn: '{{count,number}}日後にリセット',
|
||||
},
|
||||
triggerLimitModal: {
|
||||
title: 'さらにトリガーイベントを利用するにはアップグレードしてください',
|
||||
description: 'このプランのワークフロー・トリガーイベントの上限に達しました。',
|
||||
dismiss: '閉じる',
|
||||
upgrade: 'アップグレード',
|
||||
usageTitle: 'TRIGGER EVENTS',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: 'プランをアップグレード',
|
||||
@ -67,10 +59,10 @@ const translation = {
|
||||
documentsTooltip: 'ナレッジデータソースからインポートされたドキュメントの数に対するクォータ。',
|
||||
vectorSpace: '{{size}}のナレッジベースのデータストレージ',
|
||||
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
|
||||
documentsRequestQuota: '{{count,number}} のナレッジリクエスト上限 / 分',
|
||||
documentsRequestQuota: '{{count,number}}/分のナレッジ リクエストのレート制限',
|
||||
documentsRequestQuotaTooltip: 'ナレッジベース内でワークスペースが 1 分間に実行できる操作の総数を示します。これには、データセットの作成、削除、更新、ドキュメントのアップロード、修正、アーカイブ、およびナレッジベースクエリが含まれます。この指標は、ナレッジベースリクエストのパフォーマンスを評価するために使用されます。例えば、Sandbox ユーザーが 1 分間に 10 回連続でヒットテストを実行した場合、そのワークスペースは次の 1 分間、データセットの作成、削除、更新、ドキュメントのアップロードや修正などの操作を一時的に実行できなくなります。',
|
||||
apiRateLimit: 'API リクエスト制限',
|
||||
apiRateLimitUnit: '{{count,number}} の',
|
||||
apiRateLimit: 'API レート制限',
|
||||
apiRateLimitUnit: '{{count,number}}/月',
|
||||
unlimitedApiRate: '無制限の API コール',
|
||||
apiRateLimitTooltip: 'API レート制限は、テキスト生成、チャットボット、ワークフロー、ドキュメント処理など、Dify API 経由のすべてのリクエストに適用されます。',
|
||||
documentProcessingPriority: '文書処理',
|
||||
@ -80,22 +72,6 @@ const translation = {
|
||||
'priority': '優先',
|
||||
'top-priority': '最優先',
|
||||
},
|
||||
triggerEvents: {
|
||||
sandbox: '{{count,number}} トリガーイベント数',
|
||||
professional: '{{count,number}} トリガーイベント数 / 月',
|
||||
unlimited: '無制限のトリガーイベント数',
|
||||
tooltip: 'プラグイントリガー、タイマートリガー、または Webhook トリガーによって自動的にワークフローを起動するイベントの回数です。',
|
||||
},
|
||||
workflowExecution: {
|
||||
standard: '標準ワークフロー実行キュー',
|
||||
faster: '高速ワークフロー実行キュー',
|
||||
priority: '優先度の高いワークフロー実行キュー',
|
||||
tooltip: 'ワークフローの実行キューの優先度と実行速度。',
|
||||
},
|
||||
startNodes: {
|
||||
limited: '各ワークフローにつき、開始ノードは最大{{count}}つまで設定可能',
|
||||
unlimited: '各ワークフローの開始ノード数は無制限',
|
||||
},
|
||||
logsHistory: '{{days}}のログ履歴',
|
||||
customTools: 'カスタムツール',
|
||||
unavailable: '利用不可',
|
||||
@ -123,7 +99,7 @@ const translation = {
|
||||
memberAfter: 'メンバー',
|
||||
messageRequest: {
|
||||
title: '{{count,number}}メッセージクレジット',
|
||||
titlePerMonth: '{{count,number}}メッセージクレジット / 月',
|
||||
titlePerMonth: '{{count,number}}メッセージクレジット/月',
|
||||
tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。',
|
||||
},
|
||||
annotatedResponse: {
|
||||
|
||||
@ -119,11 +119,6 @@ const translation = {
|
||||
tagBound: 'このタグを使用しているアプリの数',
|
||||
moreActions: 'さらにアクション',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: 'アップグレードして',
|
||||
startNodeTitleSuffix: '開始ノードの上限を解除',
|
||||
startNodeDesc: '現在のプランでは開始ノードは2個までです。公開するにはプランをアップグレードしてください。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境変数',
|
||||
envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: '발사',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: '백엔드 서비스 API',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: '서비스 중',
|
||||
disable: '비활성',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: '트리거 노드 모드에서는 {{feature}} 기능이 지원되지 않습니다.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: '분석',
|
||||
|
||||
@ -68,7 +68,7 @@ const translation = {
|
||||
title: '메시지 크레딧',
|
||||
tooltip:
|
||||
'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.',
|
||||
titlePerMonth: '{{count,number}} 메시지 / 월',
|
||||
titlePerMonth: '{{count,number}} 메시지/월',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: '주석 응답 쿼터',
|
||||
@ -88,7 +88,7 @@ const translation = {
|
||||
freeTrialTip: '200 회의 OpenAI 호출 무료 체험을 받으세요. ',
|
||||
annualBilling: '연간 청구',
|
||||
getStarted: '시작하기',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/월',
|
||||
freeTrialTipSuffix: '신용카드 없음',
|
||||
teamWorkspace: '{{count,number}} 팀 작업 공간',
|
||||
self: '자체 호스팅',
|
||||
|
||||
@ -125,6 +125,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Uruchomić',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API usługi w tle',
|
||||
@ -136,6 +137,10 @@ const translation = {
|
||||
running: 'W usłudze',
|
||||
disable: 'Wyłącz',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Funkcja {{feature}} nie jest obsługiwana w trybie węzła wyzwalającego.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analiza',
|
||||
|
||||
@ -68,7 +68,7 @@ const translation = {
|
||||
title: 'Limity kredytów wiadomości',
|
||||
tooltip:
|
||||
'Limity wywołań wiadomości dla różnych planów używających modeli OpenAI (z wyjątkiem gpt4). Wiadomości przekraczające limit będą korzystać z twojego klucza API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} wiadomości / miesiąc',
|
||||
titlePerMonth: '{{count,number}} wiadomości/miesiąc',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Limity kredytów na adnotacje',
|
||||
@ -91,7 +91,7 @@ const translation = {
|
||||
freeTrialTipPrefix: 'Zarejestruj się i zdobądź',
|
||||
teamMember_other: '{{count,number}} członków zespołu',
|
||||
teamWorkspace: '{{count,number}} Zespół Workspace',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/miesiąc',
|
||||
cloud: 'Usługa chmurowa',
|
||||
teamMember_one: '{{count,number}} Członek zespołu',
|
||||
priceTip: 'na przestrzeń roboczą/',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Lançar',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API de Serviço de Back-end',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'Em serviço',
|
||||
disable: 'Desabilitar',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'O recurso {{feature}} não é compatível no modo Nó de Gatilho.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Análise',
|
||||
|
||||
@ -61,7 +61,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Créditos de Mensagem',
|
||||
tooltip: 'Cotas de invocação de mensagens para vários planos usando modelos da OpenAI (exceto gpt4). Mensagens além do limite usarão sua Chave de API da OpenAI.',
|
||||
titlePerMonth: '{{count,number}} mensagens / mês',
|
||||
titlePerMonth: '{{count,number}} mensagens/mês',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Limites de Cota de Anotação',
|
||||
@ -80,7 +80,7 @@ const translation = {
|
||||
documentsRequestQuota: '{{count,number}}/min Limite de Taxa de Solicitação de Conhecimento',
|
||||
cloud: 'Serviço de Nuvem',
|
||||
teamWorkspace: '{{count,number}} Espaço de Trabalho da Equipe',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/mês',
|
||||
freeTrialTipSuffix: 'Nenhum cartão de crédito necessário',
|
||||
teamMember_other: '{{count,number}} Membros da Equipe',
|
||||
comparePlanAndFeatures: 'Compare planos e recursos',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Lansa',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API serviciu backend',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'În service',
|
||||
disable: 'Dezactivat',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Funcționalitatea {{feature}} nu este suportată în modul Nod Trigger.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analiză',
|
||||
|
||||
@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Credite de mesaje',
|
||||
tooltip: 'Cote de invocare a mesajelor pentru diferite planuri utilizând modele OpenAI (cu excepția gpt4). Mesajele peste limită vor utiliza cheia API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} mesaje / lună',
|
||||
titlePerMonth: '{{count,number}} mesaje/lună',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Limite de cotă de anotare',
|
||||
@ -82,7 +82,7 @@ const translation = {
|
||||
documentsTooltip: 'Cota pe numărul de documente importate din Sursele de Date de Cunoștințe.',
|
||||
getStarted: 'Întrebați-vă',
|
||||
cloud: 'Serviciu de cloud',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/lună',
|
||||
comparePlanAndFeatures: 'Compară planurile și caracteristicile',
|
||||
documentsRequestQuota: '{{count,number}}/min Limita de rată a cererilor de cunoștințe',
|
||||
documents: '{{count,number}} Documente de Cunoaștere',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Баркас',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API серверной части',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'В работе',
|
||||
disable: 'Отключено',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Функция {{feature}} не поддерживается в режиме узла триггера.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Анализ',
|
||||
|
||||
@ -65,7 +65,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Кредиты на сообщения',
|
||||
tooltip: 'Квоты вызова сообщений для различных тарифных планов, использующих модели OpenAI (кроме gpt4). Сообщения, превышающие лимит, будут использовать ваш ключ API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} сообщений / месяц',
|
||||
titlePerMonth: '{{count,number}} сообщений/месяц',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Ограничения квоты аннотаций',
|
||||
@ -78,7 +78,7 @@ const translation = {
|
||||
apiRateLimit: 'Ограничение скорости API',
|
||||
self: 'Самостоятельно размещенный',
|
||||
teamMember_other: '{{count,number}} Члены команды',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/месяц',
|
||||
unlimitedApiRate: 'Нет ограничений на количество запросов к API',
|
||||
freeTrialTip: 'бесплатная пробная версия из 200 вызовов OpenAI.',
|
||||
freeTrialTipSuffix: 'Кредитная карта не требуется',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Začetek',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API storitev v ozadju',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'V storitvi',
|
||||
disable: 'Onemogočeno',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Funkcija {{feature}} ni podprta v načinu vozlišča sprožilca.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analiza',
|
||||
|
||||
@ -65,7 +65,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Krediti za sporočila',
|
||||
tooltip: 'Kvota za klice sporočil pri različnih načrtih z uporabo modelov OpenAI (razen GPT-4). Sporočila preko omejitve bodo uporabljala vaš OpenAI API ključ.',
|
||||
titlePerMonth: '{{count,number}} sporočil / mesec',
|
||||
titlePerMonth: '{{count,number}} sporočil/mesec',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Omejitve kvote za označevanje',
|
||||
@ -86,7 +86,7 @@ const translation = {
|
||||
teamMember_one: '{{count,number}} član ekipe',
|
||||
teamMember_other: '{{count,number}} Članov ekipe',
|
||||
documentsRequestQuota: '{{count,number}}/min Omejitev stopnje zahtev po znanju',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/mesec',
|
||||
priceTip: 'na delovnem prostoru/',
|
||||
freeTrialTipPrefix: 'Prijavite se in prejmite',
|
||||
cloud: 'Oblačna storitev',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'เรือยนต์',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API บริการแบ็กเอนด์',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'ให้บริการ',
|
||||
disable: 'พิการ',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'โหมดโหนดทริกเกอร์ไม่รองรับฟีเจอร์ {{feature}}.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'การวิเคราะห์',
|
||||
|
||||
@ -65,7 +65,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'เครดิตข้อความ',
|
||||
tooltip: 'โควต้าการเรียกใช้ข้อความสําหรับแผนต่างๆ โดยใช้โมเดล OpenAI (ยกเว้น gpt4) ข้อความที่เกินขีดจํากัดจะใช้คีย์ OpenAI API ของคุณ',
|
||||
titlePerMonth: '{{count,number}} ข้อความ / เดือน',
|
||||
titlePerMonth: '{{count,number}} ข้อความ/เดือน',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'ขีดจํากัดโควต้าคําอธิบายประกอบ',
|
||||
@ -82,7 +82,7 @@ const translation = {
|
||||
teamMember_one: '{{count,number}} สมาชิกทีม',
|
||||
unlimitedApiRate: 'ไม่มีข้อจำกัดอัตราการเรียก API',
|
||||
self: 'โฮสต์ด้วยตัวเอง',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/เดือน',
|
||||
teamMember_other: '{{count,number}} สมาชิกทีม',
|
||||
teamWorkspace: '{{count,number}} ทีมทำงาน',
|
||||
priceTip: 'ต่อพื้นที่ทำงาน/',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Başlat',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'Arka Uç Servis API\'si',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'Hizmette',
|
||||
disable: 'Devre Dışı',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Trigger Düğümü modunda {{feature}} özelliği desteklenmiyor.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Analiz',
|
||||
|
||||
@ -65,7 +65,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Mesaj Kredileri',
|
||||
tooltip: 'OpenAI modellerini (gpt4 hariç) kullanarak çeşitli planlar için mesaj çağrı kotaları. Limitin üzerindeki mesajlar OpenAI API Anahtarınızı kullanır.',
|
||||
titlePerMonth: '{{count,number}} mesaj / ay',
|
||||
titlePerMonth: '{{count,number}} mesaj/ay',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Ek Açıklama Kota Sınırları',
|
||||
@ -78,7 +78,7 @@ const translation = {
|
||||
freeTrialTipPrefix: 'Kaydolun ve bir',
|
||||
priceTip: 'iş alanı başına/',
|
||||
documentsRequestQuota: '{{count,number}}/dakika Bilgi İsteği Oran Limiti',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/ay',
|
||||
documents: '{{count,number}} Bilgi Belgesi',
|
||||
comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır',
|
||||
self: 'Kendi Barındırılan',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Запуску',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API сервісу Backend',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'У роботі',
|
||||
disable: 'Вимкнути',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Функція {{feature}} не підтримується в режимі вузла тригера.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Аналіз',
|
||||
|
||||
@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Кредити повідомлень',
|
||||
tooltip: 'Квоти на виклик повідомлень для різних планів з використанням моделей OpenAI (крім gpt4). Повідомлення понад ліміт використовуватимуть ваш ключ API OpenAI.',
|
||||
titlePerMonth: '{{count,number}} повідомлень / місяць',
|
||||
titlePerMonth: '{{count,number}} повідомлень/місяць',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Ліміти квоти відповідей з анотаціями',
|
||||
@ -84,7 +84,7 @@ const translation = {
|
||||
priceTip: 'за робочим простором/',
|
||||
unlimitedApiRate: 'Немає обмеження на швидкість API',
|
||||
freeTrialTipSuffix: 'Кредитна картка не потрібна',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/місяць',
|
||||
getStarted: 'Почати',
|
||||
freeTrialTip: 'безкоштовна пробна версія з 200 запитів до OpenAI.',
|
||||
documents: '{{count,number}} Документів знань',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: 'Phóng',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: 'API dịch vụ backend',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: 'Đang hoạt động',
|
||||
disable: 'Đã tắt',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: 'Tính năng {{feature}} không được hỗ trợ trong chế độ Nút Kích hoạt.',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: 'Phân tích',
|
||||
|
||||
@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: 'Số Lượng Tin Nhắn',
|
||||
tooltip: 'Hạn mức triệu hồi tin nhắn cho các kế hoạch sử dụng mô hình OpenAI (ngoại trừ gpt4). Các tin nhắn vượt quá giới hạn sẽ sử dụng Khóa API OpenAI của bạn.',
|
||||
titlePerMonth: '{{count,number}} tin nhắn / tháng',
|
||||
titlePerMonth: '{{count,number}} tin nhắn/tháng',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: 'Hạn Mức Quota Phản hồi Đã được Ghi chú',
|
||||
@ -90,7 +90,7 @@ const translation = {
|
||||
teamMember_other: '{{count,number}} thành viên trong nhóm',
|
||||
documents: '{{count,number}} Tài liệu Kiến thức',
|
||||
getStarted: 'Bắt đầu',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
apiRateLimitUnit: '{{count,number}}/tháng',
|
||||
freeTrialTipSuffix: 'Không cần thẻ tín dụng',
|
||||
documentsRequestQuotaTooltip: 'Chỉ định tổng số hành động mà một không gian làm việc có thể thực hiện mỗi phút trong cơ sở tri thức, bao gồm tạo mới tập dữ liệu, xóa, cập nhật, tải tài liệu lên, thay đổi, lưu trữ và truy vấn cơ sở tri thức. Chỉ số này được sử dụng để đánh giá hiệu suất của các yêu cầu cơ sở tri thức. Ví dụ, nếu một người dùng Sandbox thực hiện 10 lần kiểm tra liên tiếp trong một phút, không gian làm việc của họ sẽ bị hạn chế tạm thời không thực hiện các hành động sau trong phút tiếp theo: tạo mới tập dữ liệu, xóa, cập nhật và tải tài liệu lên hoặc thay đổi.',
|
||||
startBuilding: 'Bắt đầu xây dựng',
|
||||
|
||||
@ -7,16 +7,8 @@ const translation = {
|
||||
documentsUploadQuota: '文档上传配额',
|
||||
vectorSpace: '知识库数据存储空间',
|
||||
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
|
||||
triggerEvents: '触发器事件数',
|
||||
triggerEvents: '触发事件',
|
||||
perMonth: '每月',
|
||||
resetsIn: '{{count,number}} 天后重置',
|
||||
},
|
||||
triggerLimitModal: {
|
||||
title: '升级以解锁更多触发事件额度',
|
||||
description: '当前套餐的工作流触发事件额度已达上限。',
|
||||
dismiss: '知道了',
|
||||
upgrade: '升级',
|
||||
usageTitle: '触发事件额度',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: '查看套餐',
|
||||
@ -68,10 +60,10 @@ const translation = {
|
||||
documentsTooltip: '从知识库的数据源导入的文档数量配额。',
|
||||
vectorSpace: '{{size}} 知识库数据存储空间',
|
||||
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
|
||||
documentsRequestQuota: '{{count,number}} 知识请求 / 分钟',
|
||||
documentsRequestQuota: '{{count,number}}/分钟 知识库请求频率限制',
|
||||
documentsRequestQuotaTooltip: '指每分钟内,一个空间在知识库中可执行的操作总数,包括数据集的创建、删除、更新,文档的上传、修改、归档,以及知识库查询等,用于评估知识库请求的性能。例如,Sandbox 用户在 1 分钟内连续执行 10 次命中测试,其工作区将在接下来的 1 分钟内无法继续执行以下操作:数据集的创建、删除、更新,文档的上传、修改等操作。',
|
||||
apiRateLimit: 'API 请求频率限制',
|
||||
apiRateLimitUnit: '{{count,number}} 次',
|
||||
apiRateLimitUnit: '{{count,number}} 次/月',
|
||||
unlimitedApiRate: 'API 请求频率无限制',
|
||||
apiRateLimitTooltip: 'API 请求频率限制涵盖所有通过 Dify API 发起的调用,例如文本生成、聊天对话、工作流执行和文档处理等。',
|
||||
documentProcessingPriority: '文档处理',
|
||||
@ -82,20 +74,18 @@ const translation = {
|
||||
'top-priority': '最高优先级',
|
||||
},
|
||||
triggerEvents: {
|
||||
sandbox: '{{count,number}} 触发器事件数',
|
||||
professional: '{{count,number}} 触发器事件数 / 月',
|
||||
unlimited: '无限触发器事件数',
|
||||
tooltip: '通过插件、定时触发器、Webhook 等来自动触发工作流的事件数。',
|
||||
sandbox: '{{count,number}} 触发事件',
|
||||
professional: '{{count,number}} 触发事件/月',
|
||||
unlimited: '无限制触发事件',
|
||||
},
|
||||
workflowExecution: {
|
||||
standard: '标准工作流执行队列',
|
||||
faster: '快速工作流执行队列',
|
||||
priority: '高优先级工作流执行队列',
|
||||
tooltip: '工作流的执行队列优先级与运行速度。',
|
||||
standard: '标准工作流执行',
|
||||
faster: '更快的工作流执行',
|
||||
priority: '优先工作流执行',
|
||||
},
|
||||
startNodes: {
|
||||
limited: '最多 {{count}} 个起始节点 / 工作流',
|
||||
unlimited: '无限的起始节点 / 工作流',
|
||||
limited: '每个工作流最多 {{count}} 个起始节点',
|
||||
unlimited: '每个工作流无限制起始节点',
|
||||
},
|
||||
logsHistory: '{{days}}日志历史',
|
||||
customTools: '自定义工具',
|
||||
@ -124,7 +114,7 @@ const translation = {
|
||||
memberAfter: '个成员',
|
||||
messageRequest: {
|
||||
title: '{{count,number}} 条消息额度',
|
||||
titlePerMonth: '{{count,number}} 条消息额度 / 月',
|
||||
titlePerMonth: '{{count,number}} 条消息额度/月',
|
||||
tooltip: '消息额度旨在帮助您便捷地试用 Dify 中的各类 OpenAI 模型。不同模型会消耗不同额度。额度用尽后,您可以切换为使用自己的 OpenAI API 密钥。',
|
||||
},
|
||||
annotatedResponse: {
|
||||
|
||||
@ -122,11 +122,6 @@ const translation = {
|
||||
noHistory: '没有历史版本',
|
||||
tagBound: '使用此标签的应用数量',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: '升级以',
|
||||
startNodeTitleSuffix: '解锁无限开始节点',
|
||||
startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '环境变量',
|
||||
envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。',
|
||||
|
||||
@ -114,6 +114,7 @@ const translation = {
|
||||
},
|
||||
},
|
||||
launch: '發射',
|
||||
enableTooltip: {},
|
||||
},
|
||||
apiInfo: {
|
||||
title: '後端服務 API',
|
||||
@ -125,6 +126,10 @@ const translation = {
|
||||
running: '執行中',
|
||||
disable: '已停用',
|
||||
},
|
||||
triggerInfo: {},
|
||||
disableTooltip: {
|
||||
triggerMode: '觸發節點模式不支援 {{feature}} 功能。',
|
||||
},
|
||||
},
|
||||
analysis: {
|
||||
title: '分析',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user