mirror of
https://github.com/langgenius/dify.git
synced 2025-12-20 14:42:37 +00:00
Compare commits
36 Commits
refactor/u
...
feat/trigg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d7c99ab7 | ||
|
|
de2a469048 | ||
|
|
1730572498 | ||
|
|
9e763e80e8 | ||
|
|
196bf3d9a0 | ||
|
|
c78fbabe60 | ||
|
|
7666013227 | ||
|
|
20b1b1bf43 | ||
|
|
55e536b1e0 | ||
|
|
e6bc5a9629 | ||
|
|
f021b8248e | ||
|
|
0ac9b308cf | ||
|
|
6e5486b556 | ||
|
|
0f6e058c73 | ||
|
|
4bb225c7ee | ||
|
|
79fdc5b07b | ||
|
|
e7794be27a | ||
|
|
ae36958ef4 | ||
|
|
86bc2924f3 | ||
|
|
318b9d707b | ||
|
|
9174597eb8 | ||
|
|
0e689b14a6 | ||
|
|
9c09c993ba | ||
|
|
7a810a4412 | ||
|
|
639a78fc7b | ||
|
|
dfff9ec00a | ||
|
|
d16d61425f | ||
|
|
3c06b62fc5 | ||
|
|
5ffe7f8c0c | ||
|
|
87954a8226 | ||
|
|
6430e014b0 | ||
|
|
494f6b06e1 | ||
|
|
404240baf9 | ||
|
|
8fb027d331 | ||
|
|
d3d3868b4a | ||
|
|
7c5a008f5c |
@@ -38,6 +38,12 @@ 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):
|
||||
@@ -445,6 +451,78 @@ 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",
|
||||
|
||||
178
api/templates/api_rate_limit_limit_template_en-US.html
Normal file
178
api/templates/api_rate_limit_limit_template_en-US.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!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>
|
||||
177
api/templates/api_rate_limit_limit_template_zh-CN.html
Normal file
177
api/templates/api_rate_limit_limit_template_zh-CN.html
Normal file
@@ -0,0 +1,177 @@
|
||||
<!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>
|
||||
179
api/templates/api_rate_limit_warning_template_en-US.html
Normal file
179
api/templates/api_rate_limit_warning_template_en-US.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!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>
|
||||
177
api/templates/api_rate_limit_warning_template_zh-CN.html
Normal file
177
api/templates/api_rate_limit_warning_template_zh-CN.html
Normal file
@@ -0,0 +1,177 @@
|
||||
<!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>
|
||||
184
api/templates/trigger_events_limit_template_en-US.html
Normal file
184
api/templates/trigger_events_limit_template_en-US.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!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>
|
||||
184
api/templates/trigger_events_limit_template_zh-CN.html
Normal file
184
api/templates/trigger_events_limit_template_zh-CN.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!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>
|
||||
185
api/templates/trigger_events_usage_warning_template_en-US.html
Normal file
185
api/templates/trigger_events_usage_warning_template_en-US.html
Normal file
@@ -0,0 +1,185 @@
|
||||
<!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>
|
||||
184
api/templates/trigger_events_usage_warning_template_zh-CN.html
Normal file
184
api/templates/trigger_events_usage_warning_template_zh-CN.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,173 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,172 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,174 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,172 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,180 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!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>
|
||||
@@ -49,6 +49,7 @@ 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]: {
|
||||
@@ -106,6 +107,7 @@ 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']
|
||||
@@ -127,6 +129,7 @@ const AppPublisher = ({
|
||||
workflowToolAvailable = true,
|
||||
missingStartNode = false,
|
||||
hasTriggerNode = false,
|
||||
startNodeLimitExceeded = false,
|
||||
}: AppPublisherProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -246,6 +249,13 @@ 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 (
|
||||
<>
|
||||
@@ -304,29 +314,49 @@ 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>
|
||||
))}
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -90,4 +90,8 @@ export const defaultPlan = {
|
||||
apiRateLimit: ALL_PLANS.sandbox.apiRateLimit,
|
||||
triggerEvents: ALL_PLANS.sandbox.triggerEvents,
|
||||
},
|
||||
reset: {
|
||||
apiRateLimit: null,
|
||||
triggerEvents: null,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ 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'
|
||||
@@ -44,9 +45,20 @@ const PlanComp: FC<Props> = ({
|
||||
const {
|
||||
usage,
|
||||
total,
|
||||
reset,
|
||||
} = plan
|
||||
const perMonthUnit = ` ${t('billing.usagePage.perMonth')}`
|
||||
const triggerEventUnit = plan.type === Plan.sandbox ? undefined : perMonthUnit
|
||||
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 [showModal, setShowModal] = React.useState(false)
|
||||
const { mutateAsync } = useEducationVerify()
|
||||
@@ -79,7 +91,6 @@ 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>
|
||||
@@ -124,18 +135,20 @@ const PlanComp: FC<Props> = ({
|
||||
total={total.annotatedResponse}
|
||||
/>
|
||||
<UsageInfo
|
||||
Icon={RiFlashlightLine}
|
||||
Icon={TriggerAll}
|
||||
name={t('billing.usagePage.triggerEvents')}
|
||||
usage={usage.triggerEvents}
|
||||
total={total.triggerEvents}
|
||||
unit={triggerEventUnit}
|
||||
tooltip={t('billing.plansCommon.triggerEvents.tooltip') as string}
|
||||
resetInDays={triggerEventsResetInDays}
|
||||
/>
|
||||
<UsageInfo
|
||||
Icon={RiSpeedLine}
|
||||
Icon={ApiAggregate}
|
||||
name={t('billing.plansCommon.apiRateLimit')}
|
||||
usage={usage.apiRateLimit}
|
||||
total={total.apiRateLimit}
|
||||
unit={perMonthUnit}
|
||||
tooltip={total.apiRateLimit === NUM_INFINITE ? undefined : t('billing.plansCommon.apiRateLimitTooltip') as string}
|
||||
resetInDays={apiRateLimitResetInDays}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -46,16 +46,10 @@ 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
|
||||
@@ -64,6 +58,14 @@ 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={
|
||||
@@ -73,13 +75,7 @@ const List = ({
|
||||
? t('billing.plansCommon.workflowExecution.faster')
|
||||
: t('billing.plansCommon.workflowExecution.priority')
|
||||
}
|
||||
/>
|
||||
<Item
|
||||
label={
|
||||
plan === Plan.sandbox
|
||||
? t('billing.plansCommon.startNodes.limited', { count: 2 })
|
||||
: t('billing.plansCommon.startNodes.unlimited')
|
||||
}
|
||||
tooltip={t('billing.plansCommon.workflowExecution.tooltip') as string}
|
||||
/>
|
||||
<Divider bgStyle='gradient' />
|
||||
<Item
|
||||
@@ -89,6 +85,14 @@ 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')}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'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,6 +55,11 @@ 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',
|
||||
@@ -91,10 +96,12 @@ 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 { FC } from 'react'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '../../base/premium-badge'
|
||||
@@ -9,19 +9,24 @@ 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()
|
||||
@@ -40,10 +45,17 @@ const UpgradeBtn: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)
|
||||
const label = labelKey ? t(labelKey) : defaultBadgeLabel
|
||||
|
||||
if (isPlain) {
|
||||
return (
|
||||
<Button onClick={onClick}>
|
||||
{t('billing.upgradeBtn.plain')}
|
||||
<Button
|
||||
className={className}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{labelKey ? label : t('billing.upgradeBtn.plain')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -54,11 +66,13 @@ 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'>
|
||||
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
|
||||
@@ -16,10 +16,12 @@ type Props = {
|
||||
total: number
|
||||
unit?: string
|
||||
unitPosition?: 'inline' | 'suffix'
|
||||
resetHint?: string
|
||||
resetInDays?: number
|
||||
hideIcon?: boolean
|
||||
}
|
||||
|
||||
const LOW = 50
|
||||
const MIDDLE = 80
|
||||
const WARNING_THRESHOLD = 80
|
||||
|
||||
const UsageInfo: FC<Props> = ({
|
||||
className,
|
||||
@@ -30,28 +32,39 @@ const UsageInfo: FC<Props> = ({
|
||||
total,
|
||||
unit,
|
||||
unitPosition = 'suffix',
|
||||
resetHint,
|
||||
resetInDays,
|
||||
hideIcon = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const percent = usage / total * 100
|
||||
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 color = percent >= 100
|
||||
? 'bg-components-progress-error-progress'
|
||||
: (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid')
|
||||
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)}>
|
||||
<Icon className='h-4 w-4 text-text-tertiary' />
|
||||
{!hideIcon && Icon && (
|
||||
<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 && (
|
||||
@@ -70,11 +83,7 @@ const UsageInfo: FC<Props> = ({
|
||||
<div className='system-md-regular text-text-quaternary'>/</div>
|
||||
<div>{totalDisplay}</div>
|
||||
</div>
|
||||
{showUnit && (
|
||||
<div className='system-xs-medium ml-auto text-text-tertiary'>
|
||||
{unit}
|
||||
</div>
|
||||
)}
|
||||
{rightInfo}
|
||||
</div>
|
||||
<ProgressBar
|
||||
percent={percent}
|
||||
|
||||
@@ -36,5 +36,9 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ 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()
|
||||
@@ -50,6 +52,7 @@ 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)
|
||||
@@ -95,6 +98,15 @@ 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()
|
||||
@@ -196,7 +208,8 @@ const FeaturesTrigger = () => {
|
||||
crossAxisOffset: 4,
|
||||
missingStartNode: !startNode,
|
||||
hasTriggerNode,
|
||||
publishDisabled: !hasWorkflowNodes,
|
||||
startNodeLimitExceeded,
|
||||
publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
130
web/context/hooks/use-trigger-events-limit-modal.ts
Normal file
130
web/context/hooks/use-trigger-events-limit-modal.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
181
web/context/modal-context.test.tsx
Normal file
181
web/context/modal-context.test.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
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,6 +36,12 @@ 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,
|
||||
@@ -74,6 +80,9 @@ 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
|
||||
@@ -113,6 +122,7 @@ 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'
|
||||
@@ -130,6 +140,7 @@ const ModalContext = createContext<ModalContextState>({
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
setShowEducationExpireNoticeModal: noop,
|
||||
setShowTriggerEventsLimitModal: noop,
|
||||
})
|
||||
|
||||
export const useModalContext = () => useContext(ModalContext)
|
||||
@@ -168,6 +179,7 @@ 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,
|
||||
@@ -228,6 +240,17 @@ 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)
|
||||
@@ -334,6 +357,7 @@ export const ModalContextProvider = ({
|
||||
setShowOpeningModal,
|
||||
setShowUpdatePluginModal,
|
||||
setShowEducationExpireNoticeModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
}}>
|
||||
<>
|
||||
{children}
|
||||
@@ -455,6 +479,25 @@ 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 } from '@/app/components/billing/type'
|
||||
import type { Plan, UsageResetInfo } 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,6 +40,7 @@ type ProviderContextState = {
|
||||
type: Plan
|
||||
usage: UsagePlanInfo
|
||||
total: UsagePlanInfo
|
||||
reset: UsageResetInfo
|
||||
}
|
||||
isFetchedPlan: boolean
|
||||
enableBilling: boolean
|
||||
|
||||
@@ -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}}/Monat',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
documentsTooltip: 'Vorgabe für die Anzahl der Dokumente, die aus der Wissensdatenquelle importiert werden.',
|
||||
apiRateLimit: 'API-Datenlimit',
|
||||
documents: '{{count,number}} Wissensdokumente',
|
||||
|
||||
@@ -9,8 +9,16 @@ 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',
|
||||
@@ -61,11 +69,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}}/min Knowledge Request Rate Limit',
|
||||
documentsRequestQuota: '{{count,number}} Knowledge Request / min',
|
||||
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}}/month',
|
||||
unlimitedApiRate: 'No API Rate Limit',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
unlimitedApiRate: 'No Dify 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.',
|
||||
@@ -76,17 +84,19 @@ 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 per Workflow',
|
||||
unlimited: 'Unlimited Start Nodes per Workflow',
|
||||
limited: 'Up to {{count}} Start Nodes / workflow',
|
||||
unlimited: 'Unlimited Start Nodes / workflow',
|
||||
},
|
||||
logsHistory: '{{days}} Log history',
|
||||
customTools: 'Custom Tools',
|
||||
@@ -115,7 +125,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,6 +123,11 @@ 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.',
|
||||
|
||||
@@ -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}}/mes',
|
||||
apiRateLimitUnit: '{{count, número}}',
|
||||
freeTrialTipSuffix: 'No se requiere tarjeta de crédito',
|
||||
unlimitedApiRate: 'Sin límite de tasa de API',
|
||||
apiRateLimit: 'Límite de tasa de API',
|
||||
|
||||
@@ -73,7 +73,7 @@ const translation = {
|
||||
},
|
||||
ragAPIRequestTooltip: 'به تعداد درخواستهای API که فقط قابلیتهای پردازش پایگاه دانش Dify را فراخوانی میکنند اشاره دارد.',
|
||||
receiptInfo: 'فقط صاحب تیم و مدیر تیم میتوانند اشتراک تهیه کنند و اطلاعات صورتحساب را مشاهده کنند',
|
||||
apiRateLimitUnit: '{{count,number}}/ماه',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
cloud: 'سرویس ابری',
|
||||
documents: '{{count,number}} سندهای دانش',
|
||||
self: 'خود میزبان',
|
||||
|
||||
@@ -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}}/mois',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
priceTip: 'par espace de travail/',
|
||||
freeTrialTipSuffix: 'Aucune carte de crédit requise',
|
||||
teamWorkspace: '{{count,number}} Espace de travail d\'équipe',
|
||||
|
||||
@@ -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}} टीम सदस्य',
|
||||
|
||||
@@ -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}}/mese',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
documentsRequestQuota: '{{count,number}}/min Limite di richiesta di conoscenza',
|
||||
teamMember_other: '{{count,number}} membri del team',
|
||||
freeTrialTip: 'prova gratuita di 200 chiamate OpenAI.',
|
||||
|
||||
@@ -7,8 +7,16 @@ const translation = {
|
||||
documentsUploadQuota: 'ドキュメント・アップロード・クォータ',
|
||||
vectorSpace: 'ナレッジベースのデータストレージ',
|
||||
vectorSpaceTooltip: '高品質インデックスモードのドキュメントは、ナレッジベースのデータストレージのリソースを消費します。ナレッジベースのデータストレージの上限に達すると、新しいドキュメントはアップロードされません。',
|
||||
triggerEvents: 'トリガーイベント',
|
||||
triggerEvents: 'トリガーイベント数',
|
||||
perMonth: '月あたり',
|
||||
resetsIn: '{{count,number}}日後にリセット',
|
||||
},
|
||||
triggerLimitModal: {
|
||||
title: 'さらにトリガーイベントを利用するにはアップグレードしてください',
|
||||
description: 'このプランのワークフロー・トリガーイベントの上限に達しました。',
|
||||
dismiss: '閉じる',
|
||||
upgrade: 'アップグレード',
|
||||
usageTitle: 'TRIGGER EVENTS',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: 'プランをアップグレード',
|
||||
@@ -59,10 +67,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: '文書処理',
|
||||
@@ -72,6 +80,22 @@ 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: '利用不可',
|
||||
@@ -99,7 +123,7 @@ const translation = {
|
||||
memberAfter: 'メンバー',
|
||||
messageRequest: {
|
||||
title: '{{count,number}}メッセージクレジット',
|
||||
titlePerMonth: '{{count,number}}メッセージクレジット/月',
|
||||
titlePerMonth: '{{count,number}}メッセージクレジット / 月',
|
||||
tooltip: 'メッセージクレジットは、Dify でさまざまな OpenAI モデルを簡単にお試しいただくためのものです。モデルタイプに応じてクレジットが消費され、使い切った後はご自身の OpenAI API キーに切り替えていただけます。',
|
||||
},
|
||||
annotatedResponse: {
|
||||
|
||||
@@ -119,6 +119,11 @@ const translation = {
|
||||
tagBound: 'このタグを使用しているアプリの数',
|
||||
moreActions: 'さらにアクション',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: 'アップグレードして',
|
||||
startNodeTitleSuffix: '開始ノードの上限を解除',
|
||||
startNodeDesc: '現在のプランでは開始ノードは2個までです。公開するにはプランをアップグレードしてください。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境変数',
|
||||
envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。',
|
||||
|
||||
@@ -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: '자체 호스팅',
|
||||
|
||||
@@ -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}}/miesiąc',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
cloud: 'Usługa chmurowa',
|
||||
teamMember_one: '{{count,number}} Członek zespołu',
|
||||
priceTip: 'na przestrzeń roboczą/',
|
||||
|
||||
@@ -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}}/mês',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
freeTrialTipSuffix: 'Nenhum cartão de crédito necessário',
|
||||
teamMember_other: '{{count,number}} Membros da Equipe',
|
||||
comparePlanAndFeatures: 'Compare planos e recursos',
|
||||
|
||||
@@ -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}}/lună',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
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',
|
||||
|
||||
@@ -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: 'Кредитная карта не требуется',
|
||||
|
||||
@@ -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}}/mesec',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
priceTip: 'na delovnem prostoru/',
|
||||
freeTrialTipPrefix: 'Prijavite se in prejmite',
|
||||
cloud: 'Oblačna storitev',
|
||||
|
||||
@@ -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: 'ต่อพื้นที่ทำงาน/',
|
||||
|
||||
@@ -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}}/ay',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
documents: '{{count,number}} Bilgi Belgesi',
|
||||
comparePlanAndFeatures: 'Planları ve özellikleri karşılaştır',
|
||||
self: 'Kendi Barındırılan',
|
||||
|
||||
@@ -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}} Документів знань',
|
||||
|
||||
@@ -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}}/tháng',
|
||||
apiRateLimitUnit: '{{count,number}}',
|
||||
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,8 +7,16 @@ const translation = {
|
||||
documentsUploadQuota: '文档上传配额',
|
||||
vectorSpace: '知识库数据存储空间',
|
||||
vectorSpaceTooltip: '采用高质量索引模式的文档会消耗知识数据存储资源。当知识数据存储达到限制时,将不会上传新文档。',
|
||||
triggerEvents: '触发事件',
|
||||
triggerEvents: '触发器事件数',
|
||||
perMonth: '每月',
|
||||
resetsIn: '{{count,number}} 天后重置',
|
||||
},
|
||||
triggerLimitModal: {
|
||||
title: '升级以解锁更多触发事件额度',
|
||||
description: '当前套餐的工作流触发事件额度已达上限。',
|
||||
dismiss: '知道了',
|
||||
upgrade: '升级',
|
||||
usageTitle: '触发事件额度',
|
||||
},
|
||||
upgradeBtn: {
|
||||
plain: '查看套餐',
|
||||
@@ -60,10 +68,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: '文档处理',
|
||||
@@ -74,18 +82,20 @@ const translation = {
|
||||
'top-priority': '最高优先级',
|
||||
},
|
||||
triggerEvents: {
|
||||
sandbox: '{{count,number}} 触发事件',
|
||||
professional: '{{count,number}} 触发事件/月',
|
||||
unlimited: '无限制触发事件',
|
||||
sandbox: '{{count,number}} 触发器事件数',
|
||||
professional: '{{count,number}} 触发器事件数 / 月',
|
||||
unlimited: '无限触发器事件数',
|
||||
tooltip: '通过插件、定时触发器、Webhook 等来自动触发工作流的事件数。',
|
||||
},
|
||||
workflowExecution: {
|
||||
standard: '标准工作流执行',
|
||||
faster: '更快的工作流执行',
|
||||
priority: '优先工作流执行',
|
||||
standard: '标准工作流执行队列',
|
||||
faster: '快速工作流执行队列',
|
||||
priority: '高优先级工作流执行队列',
|
||||
tooltip: '工作流的执行队列优先级与运行速度。',
|
||||
},
|
||||
startNodes: {
|
||||
limited: '每个工作流最多 {{count}} 个起始节点',
|
||||
unlimited: '每个工作流无限制起始节点',
|
||||
limited: '最多 {{count}} 个起始节点 / 工作流',
|
||||
unlimited: '无限的起始节点 / 工作流',
|
||||
},
|
||||
logsHistory: '{{days}}日志历史',
|
||||
customTools: '自定义工具',
|
||||
@@ -114,7 +124,7 @@ const translation = {
|
||||
memberAfter: '个成员',
|
||||
messageRequest: {
|
||||
title: '{{count,number}} 条消息额度',
|
||||
titlePerMonth: '{{count,number}} 条消息额度/月',
|
||||
titlePerMonth: '{{count,number}} 条消息额度 / 月',
|
||||
tooltip: '消息额度旨在帮助您便捷地试用 Dify 中的各类 OpenAI 模型。不同模型会消耗不同额度。额度用尽后,您可以切换为使用自己的 OpenAI API 密钥。',
|
||||
},
|
||||
annotatedResponse: {
|
||||
|
||||
@@ -122,6 +122,11 @@ const translation = {
|
||||
noHistory: '没有历史版本',
|
||||
tagBound: '使用此标签的应用数量',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: '升级以',
|
||||
startNodeTitleSuffix: '解锁无限开始节点',
|
||||
startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '环境变量',
|
||||
envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。',
|
||||
|
||||
@@ -64,7 +64,7 @@ const translation = {
|
||||
messageRequest: {
|
||||
title: '訊息額度',
|
||||
tooltip: '為不同方案提供基於 OpenAI 模型的訊息響應額度。',
|
||||
titlePerMonth: '{{count,number}} 消息/月',
|
||||
titlePerMonth: '{{count,number}} 消息 / 月',
|
||||
},
|
||||
annotatedResponse: {
|
||||
title: '標註回覆數',
|
||||
@@ -74,7 +74,7 @@ const translation = {
|
||||
receiptInfo: '只有團隊所有者和團隊管理員才能訂閱和檢視賬單資訊',
|
||||
annotationQuota: '註釋配額',
|
||||
self: '自我主持',
|
||||
apiRateLimitUnit: '{{count,number}}/月',
|
||||
apiRateLimitUnit: '{{count,number}} 次',
|
||||
freeTrialTipPrefix: '註冊並獲得一個',
|
||||
annualBilling: '年度計費',
|
||||
freeTrialTipSuffix: '無需信用卡',
|
||||
|
||||
@@ -116,6 +116,11 @@ const translation = {
|
||||
currentWorkflow: '當前工作流程',
|
||||
moreActions: '更多動作',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: '升級以',
|
||||
startNodeTitleSuffix: '解鎖無限開始節點',
|
||||
startNodeDesc: '目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境變數',
|
||||
envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。',
|
||||
|
||||
@@ -10,3 +10,10 @@ export const isAfter = (date: ConfigType, compare: ConfigType) => {
|
||||
export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => {
|
||||
return dayjs(date).format(dateFormat)
|
||||
}
|
||||
|
||||
export const getDaysUntilEndOfMonth = (date: ConfigType = dayjs()) => {
|
||||
const current = dayjs(date).startOf('day')
|
||||
const endOfMonth = dayjs(date).endOf('month').startOf('day')
|
||||
const diff = endOfMonth.diff(current, 'day')
|
||||
return Math.max(diff, 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user