Compare commits

...

13 Commits

@ -1,7 +1,7 @@
![cover-v5-optimized](./images/GitHub_README_if.png) ![cover-v5-optimized](./images/GitHub_README_if.png)
<p align="center"> <p align="center">
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast</a> 📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast111</a>
</p> </p>
<p align="center"> <p align="center">

@ -113,3 +113,9 @@ class MemberNotInTenantError(BaseHTTPException):
error_code = "member_not_in_tenant" error_code = "member_not_in_tenant"
description = "The member is not in the workspace." description = "The member is not in the workspace."
code = 400 code = 400
class AccountInFreezeError(BaseHTTPException):
error_code = "account_in_freeze"
description = "This email is temporarily unavailable."
code = 400

@ -9,6 +9,7 @@ from configs import dify_config
from constants.languages import supported_language from constants.languages import supported_language
from controllers.console import api from controllers.console import api
from controllers.console.auth.error import ( from controllers.console.auth.error import (
AccountInFreezeError,
EmailAlreadyInUseError, EmailAlreadyInUseError,
EmailChangeLimitError, EmailChangeLimitError,
EmailCodeError, EmailCodeError,
@ -479,21 +480,28 @@ class ChangeEmailResetApi(Resource):
parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args() args = parser.parse_args()
if AccountService.is_account_in_freeze(args["new_email"]):
raise AccountInFreezeError()
if not AccountService.check_email_unique(args["new_email"]):
raise EmailAlreadyInUseError()
reset_data = AccountService.get_change_email_data(args["token"]) reset_data = AccountService.get_change_email_data(args["token"])
if not reset_data: if not reset_data:
raise InvalidTokenError() raise InvalidTokenError()
AccountService.revoke_change_email_token(args["token"]) AccountService.revoke_change_email_token(args["token"])
if not AccountService.check_email_unique(args["new_email"]):
raise EmailAlreadyInUseError()
old_email = reset_data.get("old_email", "") old_email = reset_data.get("old_email", "")
if current_user.email != old_email: if current_user.email != old_email:
raise AccountNotFound() raise AccountNotFound()
updated_account = AccountService.update_account(current_user, email=args["new_email"]) updated_account = AccountService.update_account(current_user, email=args["new_email"])
AccountService.send_change_email_completed_notify_email(
email=args["new_email"],
)
return updated_account return updated_account

@ -1011,7 +1011,9 @@ class ToolManager:
if variable is None: if variable is None:
raise ToolParameterError(f"Variable {tool_input.value} does not exist") raise ToolParameterError(f"Variable {tool_input.value} does not exist")
parameter_value = variable.value parameter_value = variable.value
elif tool_input.type in {"mixed", "constant"}: elif tool_input.type == "constant":
parameter_value = tool_input.value
elif tool_input.type == "mixed":
segment_group = variable_pool.convert_template(str(tool_input.value)) segment_group = variable_pool.convert_template(str(tool_input.value))
parameter_value = segment_group.text parameter_value = segment_group.text
else: else:

@ -54,7 +54,7 @@ class ToolNodeData(BaseNodeData, ToolEntity):
for val in value: for val in value:
if not isinstance(val, str): if not isinstance(val, str):
raise ValueError("value must be a list of strings") raise ValueError("value must be a list of strings")
elif typ == "constant" and not isinstance(value, str | int | float | bool): elif typ == "constant" and not isinstance(value, str | int | float | bool | dict):
raise ValueError("value must be a string, int, float, or bool") raise ValueError("value must be a string, int, float, or bool")
return typ return typ

@ -25,6 +25,7 @@ class EmailType(Enum):
EMAIL_CODE_LOGIN = "email_code_login" EMAIL_CODE_LOGIN = "email_code_login"
CHANGE_EMAIL_OLD = "change_email_old" CHANGE_EMAIL_OLD = "change_email_old"
CHANGE_EMAIL_NEW = "change_email_new" CHANGE_EMAIL_NEW = "change_email_new"
CHANGE_EMAIL_COMPLETED = "change_email_completed"
OWNER_TRANSFER_CONFIRM = "owner_transfer_confirm" OWNER_TRANSFER_CONFIRM = "owner_transfer_confirm"
OWNER_TRANSFER_OLD_NOTIFY = "owner_transfer_old_notify" OWNER_TRANSFER_OLD_NOTIFY = "owner_transfer_old_notify"
OWNER_TRANSFER_NEW_NOTIFY = "owner_transfer_new_notify" OWNER_TRANSFER_NEW_NOTIFY = "owner_transfer_new_notify"
@ -344,6 +345,18 @@ def create_default_email_config() -> EmailI18nConfig:
branded_template_path="without-brand/change_mail_confirm_new_template_zh-CN.html", branded_template_path="without-brand/change_mail_confirm_new_template_zh-CN.html",
), ),
}, },
EmailType.CHANGE_EMAIL_COMPLETED: {
EmailLanguage.EN_US: EmailTemplate(
subject="Your login email has been changed",
template_path="change_mail_completed_template_en-US.html",
branded_template_path="without-brand/change_mail_completed_template_en-US.html",
),
EmailLanguage.ZH_HANS: EmailTemplate(
subject="您的登录邮箱已更改",
template_path="change_mail_completed_template_zh-CN.html",
branded_template_path="without-brand/change_mail_completed_template_zh-CN.html",
),
},
EmailType.OWNER_TRANSFER_CONFIRM: { EmailType.OWNER_TRANSFER_CONFIRM: {
EmailLanguage.EN_US: EmailTemplate( EmailLanguage.EN_US: EmailTemplate(
subject="Verify Your Request to Transfer Workspace Ownership", subject="Verify Your Request to Transfer Workspace Ownership",

@ -54,7 +54,10 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError, Workspaces
from services.feature_service import FeatureService from services.feature_service import FeatureService
from tasks.delete_account_task import delete_account_task from tasks.delete_account_task import delete_account_task
from tasks.mail_account_deletion_task import send_account_deletion_verification_code from tasks.mail_account_deletion_task import send_account_deletion_verification_code
from tasks.mail_change_mail_task import send_change_mail_task from tasks.mail_change_mail_task import (
send_change_mail_completed_notification_task,
send_change_mail_task,
)
from tasks.mail_email_code_login import send_email_code_login_mail_task from tasks.mail_email_code_login import send_email_code_login_mail_task
from tasks.mail_invite_member_task import send_invite_member_mail_task from tasks.mail_invite_member_task import send_invite_member_mail_task
from tasks.mail_owner_transfer_task import ( from tasks.mail_owner_transfer_task import (
@ -461,6 +464,22 @@ class AccountService:
cls.change_email_rate_limiter.increment_rate_limit(account_email) cls.change_email_rate_limiter.increment_rate_limit(account_email)
return token return token
@classmethod
def send_change_email_completed_notify_email(
cls,
account: Optional[Account] = None,
email: Optional[str] = None,
language: Optional[str] = "en-US",
):
account_email = account.email if account else email
if account_email is None:
raise ValueError("Email must be provided.")
send_change_mail_completed_notification_task.delay(
language=language,
to=account_email,
)
@classmethod @classmethod
def send_owner_transfer_email( def send_owner_transfer_email(
cls, cls,
@ -652,6 +671,12 @@ class AccountService:
return account return account
@classmethod
def is_account_in_freeze(cls, email: str) -> bool:
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email):
return True
return False
@staticmethod @staticmethod
@redis_fallback(default_return=None) @redis_fallback(default_return=None)
def add_login_error_rate_limit(email: str) -> None: def add_login_error_rate_limit(email: str) -> None:

@ -5,7 +5,7 @@ import click
from celery import shared_task # type: ignore from celery import shared_task # type: ignore
from extensions.ext_mail import mail from extensions.ext_mail import mail
from libs.email_i18n import get_email_i18n_service from libs.email_i18n import EmailType, get_email_i18n_service
@shared_task(queue="mail") @shared_task(queue="mail")
@ -40,3 +40,41 @@ def send_change_mail_task(language: str, to: str, code: str, phase: str) -> None
) )
except Exception: except Exception:
logging.exception("Send change email mail to {} failed".format(to)) logging.exception("Send change email mail to {} failed".format(to))
@shared_task(queue="mail")
def send_change_mail_completed_notification_task(language: str, to: str) -> None:
"""
Send change email completed notification with internationalization support.
Args:
language: Language code for email localization
to: Recipient email address
"""
if not mail.is_inited():
return
logging.info(click.style("Start change email completed notify mail to {}".format(to), fg="green"))
start_at = time.perf_counter()
try:
email_service = get_email_i18n_service()
email_service.send_email(
email_type=EmailType.CHANGE_EMAIL_COMPLETED,
language_code=language,
to=to,
template_context={
"to": to,
"email": to,
},
)
end_at = time.perf_counter()
logging.info(
click.style(
"Send change email completed mail to {} succeeded: latency: {}".format(to, end_at - start_at),
fg="green",
)
)
except Exception:
logging.exception("Send change email completed mail to {} failed".format(to))

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
min-height: 374px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
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);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.support {
color: #354052;
font-weight: 500;
text-decoration: none;
}
.support:hover {
color: #354052;
font-weight: 500;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">Your login email has been changed</p>
<div class="description">
<p class="content1">You can now log into Dify with your new email address:</p>
</div>
<div class="code-content">
<span class="code">{{email}}</span>
</div>
<p class="tips">If you did not make this change, email <a class="support" href="mailto:support@dify.ai">support@dify.ai</a>.</p>
</div>
</body>
</html>

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
min-height: 374px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
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);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.support {
color: #354052;
font-weight: 500;
text-decoration: none;
}
.support:hover {
color: #354052;
font-weight: 500;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo" />
</div>
<p class="title">您的登录邮箱已更改</p>
<div class="description">
<p class="content1">您现在可以使用新的电子邮件地址登录 Dify</p>
</div>
<div class="code-content">
<span class="code">{{email}}</span>
</div>
<p class="tips">如果您没有进行此更改,请发送电子邮件至 <a class="support" href="mailto:support@dify.ai">support@dify.ai</a></p>
</div>
</body>
</html>

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
min-height: 374px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
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);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.support {
color: #354052;
font-weight: 500;
text-decoration: none;
}
.support:hover {
color: #354052;
font-weight: 500;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">Your login email has been changed</p>
<div class="description">
<p class="content1">You can now log into {{application_title}} with your new email address:</p>
</div>
<div class="code-content">
<span class="code">{{email}}</span>
</div>
<p class="tips">If you did not make this change, please ignore this email or contact support immediately.</p>
</div>
</body>
</html>

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 504px;
min-height: 374px;
margin: 40px auto;
padding: 0 48px;
background-color: #fcfcfd;
border-radius: 16px;
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);
}
.header {
padding-top: 36px;
padding-bottom: 24px;
}
.header img {
max-width: 63px;
height: auto;
}
.title {
margin: 0;
padding-top: 8px;
padding-bottom: 16px;
color: #101828;
font-size: 24px;
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%; /* 28.8px */
}
.description {
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 16px;
padding-bottom: 12px;
}
.content2 {
margin: 0;
}
.content3 {
margin: 0;
padding-bottom: 12px;
}
.code-content {
margin-bottom: 8px;
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
}
.code {
color: #101828;
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: 700;
line-height: 36px;
}
.tips {
margin: 0;
padding-top: 12px;
padding-bottom: 16px;
color: #354052;
font-size: 14px;
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.support {
color: #354052;
font-weight: 500;
text-decoration: none;
}
.support:hover {
color: #354052;
font-weight: 500;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<p class="title">您的登录邮箱已更改</p>
<div class="description">
<p class="content1">您现在可以使用新的电子邮件地址登录 {{application_title}}</p>
</div>
<div class="code-content">
<span class="code">{{email}}</span>
</div>
<p class="tips">如果您没有进行此更改,请忽略此电子邮件或立即联系支持。</p>
</div>
</body>
</html>

@ -32,6 +32,35 @@ export type IAppDetailLayoutProps = {
appId: string appId: string
} }
const useIframeHeader = () => {
const [show, setShow] = useState(true);
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (event.data.type === "HIDDEN") setShow(() => false);
};
// 初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "SIDEBA",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return {
show,
};
};
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const { const {
children, children,
@ -56,6 +85,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon: NavIcon icon: NavIcon
selectedIcon: NavIcon selectedIcon: NavIcon
}>>([]) }>>([])
const { show } = useIframeHeader()
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [ const navs = [
@ -160,9 +190,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return ( return (
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}> <div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
{appDetail && ( {show ? appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} /> <AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
)} ): ''}
<div className="grow overflow-hidden bg-components-panel-bg"> <div className="grow overflow-hidden bg-components-panel-bg">
{children} {children}
</div> </div>

@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -9,6 +9,7 @@ import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter' import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useRouter } from 'next/navigation'
dayjs.extend(quarterOfYear) dayjs.extend(quarterOfYear)
@ -21,12 +22,44 @@ export type IChartViewProps = {
headerRight: React.ReactNode headerRight: React.ReactNode
} }
let state = 0
const useIframeRedirection = () => {
const router = useRouter()
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (
event.data.type === 'CHART_VIEW_REDIRECT'
&& event.data.redirectUrl
&& state < 3 // 防止无限重定向
) {
state++
router.replace(event.data.redirectUrl)
}
}
// 首页初始化完成后提示iframe操作
window.parent.postMessage(
{
type: 'CHART_VIEW',
},
'*',
)
window.addEventListener('message', handler)
return () => window.removeEventListener('message', handler)
}, [])
}
export default function ChartView({ appId, headerRight }: IChartViewProps) { export default function ChartView({ appId, headerRight }: IChartViewProps) {
const { t } = useTranslation() const { t } = useTranslation()
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow' const isWorkflow = appDetail?.mode === 'workflow'
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
useIframeRedirection()
const onSelect = (item: Item) => { const onSelect = (item: Item) => {
if (item.value === -1) { if (item.value === -1) {

@ -1,5 +1,5 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC, JSX } from 'react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'

@ -1,4 +1,4 @@
import type { ReactElement } from 'react' import type { JSX } from 'react'
import { cloneElement, useCallback } from 'react' import { cloneElement, useCallback } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -7,7 +7,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge
import { RiMoreLine } from '@remixicon/react' import { RiMoreLine } from '@remixicon/react'
export type Operation = { export type Operation = {
id: string; title: string; icon: ReactElement; onClick: () => void id: string; title: string; icon: JSX.Element; onClick: () => void
} }
const AppOperations = ({ operations, gap }: { const AppOperations = ({ operations, gap }: {
@ -47,7 +47,7 @@ const AppOperations = ({ operations, gap }: {
updatedEntries[id] = true updatedEntries[id] = true
width += gap + childWidth width += gap + childWidth
} }
else { else {
if (i === childrens.length - 1 && width + childWidth <= containerWidth) if (i === childrens.length - 1 && width + childWidth <= containerWidth)
updatedEntries[id] = true updatedEntries[id] = true
else else

@ -91,7 +91,7 @@ const PureSelect = ({
triggerPopupSameWidth={triggerPopupSameWidth} triggerPopupSameWidth={triggerPopupSameWidth}
> >
<PortalToFollowElemTrigger <PortalToFollowElemTrigger
onClick={() => handleOpenChange(!mergedOpen)} onClick={() => !disabled && handleOpenChange(!mergedOpen)}
asChild asChild
> >
<div <div
@ -116,7 +116,7 @@ const PureSelect = ({
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn( <PortalToFollowElemContent className={cn(
'z-10', 'z-[9999]',
popupWrapperClassName, popupWrapperClassName,
)}> )}>
<div <div

@ -70,7 +70,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false) const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile) const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
const renderHitResults = (results: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => ( const renderHitResults = (results: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => (
<div className='flex h-full flex-col rounded-t-2xl bg-background-body px-4 py-3'> <div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'>
<div className='mb-2 shrink-0 pl-2 font-semibold leading-6 text-text-primary'> <div className='mb-2 shrink-0 pl-2 font-semibold leading-6 text-text-primary'>
{t('datasetHitTesting.hit.title', { num: results.length })} {t('datasetHitTesting.hit.title', { num: results.length })}
</div> </div>
@ -93,7 +93,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
) )
const renderEmptyState = () => ( const renderEmptyState = () => (
<div className='flex h-full flex-col items-center justify-center rounded-t-2xl bg-background-body px-4 py-3'> <div className='flex h-full flex-col items-center justify-center rounded-tl-2xl bg-background-body px-4 py-3'>
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!h-14 !w-14 !bg-text-quaternary')} /> <div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!h-14 !w-14 !bg-text-quaternary')} />
<div className='mt-3 text-[13px] text-text-quaternary'> <div className='mt-3 text-[13px] text-text-quaternary'>
{t('datasetHitTesting.hit.emptyTip')} {t('datasetHitTesting.hit.emptyTip')}
@ -180,7 +180,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
<div className='flex flex-col pt-3'> <div className='flex flex-col pt-3'>
{/* {renderHitResults(generalResultData)} */} {/* {renderHitResults(generalResultData)} */}
{submitLoading {submitLoading
? <div className='flex h-full flex-col rounded-t-2xl bg-background-body px-4 py-3'> ? <div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'>
<CardSkelton /> <CardSkelton />
</div> </div>
: ( : (

@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState, useEffect } from "react";
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import s from './index.module.css' import s from './index.module.css'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
@ -9,6 +9,33 @@ type HeaderWrapperProps = {
children: React.ReactNode children: React.ReactNode
} }
const useIframeHeader = () => {
const [show, setShow] = useState(true);
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (event.data.type === "HIDDEN") setShow(() => false);
};
// 初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "HEADER",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return {
show,
};
};
const HeaderWrapper = ({ const HeaderWrapper = ({
children, children,
}: HeaderWrapperProps) => { }: HeaderWrapperProps) => {
@ -19,6 +46,7 @@ const HeaderWrapper = ({
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { show } = useIframeHeader();
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {
if (v?.type === 'workflow-canvas-maximize') if (v?.type === 'workflow-canvas-maximize')
@ -26,15 +54,22 @@ const HeaderWrapper = ({
}) })
return ( return (
<div className={classNames( <>
'sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col', {show ? (
s.header, <div
isBordered ? 'border-b border-divider-regular' : '', className={classNames(
hideHeader && inWorkflowCanvas && 'hidden', "sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col",
)} s.header,
> isBordered ? "border-b border-divider-regular" : "",
{children} hideHeader && inWorkflowCanvas && "hidden"
</div> )}
) >
{children}
</div>
) : (
""
)}
</>
);
} }
export default HeaderWrapper export default HeaderWrapper

@ -164,7 +164,7 @@ const FormInputItem: FC<Props> = ({
...value, ...value,
[variable]: { [variable]: {
...varInput, ...varInput,
...newValue, value: newValue,
}, },
}) })
} }
@ -242,7 +242,7 @@ const FormInputItem: FC<Props> = ({
<AppSelector <AppSelector
disabled={readOnly} disabled={readOnly}
scope={scope || 'all'} scope={scope || 'all'}
value={varInput as any} value={varInput?.value}
onSelect={handleAppOrModelSelect} onSelect={handleAppOrModelSelect}
/> />
)} )}
@ -251,7 +251,7 @@ const FormInputItem: FC<Props> = ({
popupClassName='!w-[387px]' popupClassName='!w-[387px]'
isAdvancedMode isAdvancedMode
isInWorkflow isInWorkflow
value={varInput} value={varInput?.value}
setModel={handleAppOrModelSelect} setModel={handleAppOrModelSelect}
readonly={readOnly} readonly={readOnly}
scope={scope} scope={scope}

@ -15,6 +15,37 @@ import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
const useIframeToken = () => {
const router = useRouter();
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (
event.data.type === "LOGIN_AND_WORKFLOW_INTO" &&
event.data.tokens &&
event.data.workflowUrl
) {
localStorage.setItem("console_token", event.data.tokens.console_token);
localStorage.setItem("refresh_token", event.data.tokens.refresh_token);
router.replace(event.data.workflowUrl);
}
};
// 首页初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "LOGIN_AND_WORKFLOW",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
};
const NormalForm = () => { const NormalForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
@ -29,6 +60,7 @@ const NormalForm = () => {
const [showORLine, setShowORLine] = useState(false) const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
const [workspaceName, setWorkSpaceName] = useState('') const [workspaceName, setWorkSpaceName] = useState('')
useIframeToken();
const isInviteLink = Boolean(invite_token && invite_token !== 'null') const isInviteLink = Boolean(invite_token && invite_token !== 'null')

@ -2,6 +2,7 @@
import type { ChatConfig } from '@/app/components/base/chat/types' import type { ChatConfig } from '@/app/components/base/chat/types'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import type { AppData, AppMeta } from '@/models/share' import type { AppData, AppMeta } from '@/models/share'
import { useGetWebAppAccessModeByCode } from '@/service/use-share' import { useGetWebAppAccessModeByCode } from '@/service/use-share'
@ -60,6 +61,8 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const redirectUrlParam = searchParams.get('redirect_url') const redirectUrlParam = searchParams.get('redirect_url')
const session = searchParams.get('session')
const sysUserId = searchParams.get('sys.user_id')
const [shareCode, setShareCode] = useState<string | null>(null) const [shareCode, setShareCode] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam)
@ -69,11 +72,22 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
updateShareCode(newShareCode) updateShareCode(newShareCode)
}, [pathname, redirectUrlParam, updateShareCode]) }, [pathname, redirectUrlParam, updateShareCode])
const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true)
useEffect(() => { useEffect(() => {
if (accessModeResult?.accessMode) if (accessModeResult?.accessMode) {
updateWebAppAccessMode(accessModeResult.accessMode) updateWebAppAccessMode(accessModeResult.accessMode)
}, [accessModeResult, updateWebAppAccessMode]) if (accessModeResult?.accessMode === AccessMode.PUBLIC && session && sysUserId) {
if (isFetching) { setIsFetchingAccessToken(true)
checkOrSetAccessToken(shareCode).finally(() => {
setIsFetchingAccessToken(false)
})
}
else {
setIsFetchingAccessToken(false)
}
}
}, [accessModeResult, updateWebAppAccessMode, setIsFetchingAccessToken, shareCode, session, sysUserId])
if (isFetching || isFetchingAccessToken) {
return <div className='flex h-full w-full items-center justify-center'> return <div className='flex h-full w-full items-center justify-center'>
<Loading /> <Loading />
</div> </div>

@ -50,24 +50,35 @@ export const loadLangResources = async (lang: string) => {
acc[camelCase(NAMESPACES[index])] = mod acc[camelCase(NAMESPACES[index])] = mod
return acc return acc
}, {} as Record<string, any>) }, {} as Record<string, any>)
return resources
}
const getFallbackTranslation = () => {
const resources = NAMESPACES.reduce((acc, ns, index) => {
acc[camelCase(NAMESPACES[index])] = require(`./en-US/${ns}`).default
return acc
}, {} as Record<string, any>)
return { return {
translation: resources, translation: resources,
} }
} }
i18n.use(initReactI18next) if (!i18n.isInitialized) {
.init({ i18n.use(initReactI18next)
lng: undefined, .init({
fallbackLng: 'en-US', lng: undefined,
}) fallbackLng: 'en-US',
resources: {
'en-US': getFallbackTranslation(),
},
})
}
export const changeLanguage = async (lng?: string) => { export const changeLanguage = async (lng?: string) => {
const resolvedLng = lng ?? 'en-US' const resolvedLng = lng ?? 'en-US'
const resources = { const resource = await loadLangResources(resolvedLng)
[resolvedLng]: await loadLangResources(resolvedLng),
}
if (!i18n.hasResourceBundle(resolvedLng, 'translation')) if (!i18n.hasResourceBundle(resolvedLng, 'translation'))
i18n.addResourceBundle(resolvedLng, 'translation', resources[resolvedLng].translation, true, true) i18n.addResourceBundle(resolvedLng, 'translation', resource, true, true)
await i18n.changeLanguage(resolvedLng) await i18n.changeLanguage(resolvedLng)
} }

@ -43,14 +43,25 @@ const nextConfig = {
search: '', search: '',
})), })),
}, },
experimental: { experimental: {},
},
// fix all before production. Now it slow the develop speed. // fix all before production. Now it slow the develop speed.
eslint: { eslint: {
// Warning: This allows production builds to successfully complete even if // Warning: This allows production builds to successfully complete even if
// your project has ESLint errors. // your project has ESLint errors.
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
dirs: ['app', 'bin', 'config', 'context', 'hooks', 'i18n', 'models', 'service', 'test', 'types', 'utils'], dirs: [
'app',
'bin',
'config',
'context',
'hooks',
'i18n',
'models',
'service',
'test',
'types',
'utils',
],
}, },
typescript: { typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors // https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
@ -67,6 +78,23 @@ const nextConfig = {
] ]
}, },
output: 'standalone', output: 'standalone',
async headers() {
return [
{
source: '/(.*)', // 匹配所有路由
headers: [
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN', // 或 ALLOWALL更宽松
},
{
key: 'Content-Security-Policy',
value: 'frame-ancestors "self" *', // 允许所有
},
],
},
]
},
} }
module.exports = withBundleAnalyzer(withMDX(nextConfig)) module.exports = withBundleAnalyzer(withMDX(nextConfig))

Loading…
Cancel
Save