Merge branch 'main' into feat/rag-pipeline

pull/21398/head
twwu 11 months ago
commit db963a638c

@ -7,6 +7,7 @@ pipx install uv
echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-web-prod="cd /workspaces/dify/web && pnpm build && pnpm start"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc

@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media
## Community & contact ## Community & contact
- [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions. - [GitHub Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
- [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). - [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
- [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community. - [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
- [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community. - [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.

@ -223,7 +223,7 @@ docker compose up -d
</a> </a>
## المجتمع والاتصال ## المجتمع والاتصال
- [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة. - [مناقشة GitHub](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
- [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). - [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
- [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. - [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
- [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع. - [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.

@ -234,7 +234,7 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন
## কমিউনিটি এবং যোগাযোগ ## কমিউনিটি এবং যোগাযোগ
- [Github Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম। - [GitHub Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম।
- [GitHub Issues](https://github.com/langgenius/dify/issues). Dify.AI ব্যবহার করে আপনি যেসব বাগের সম্মুখীন হন এবং ফিচার প্রস্তাবনা। আমাদের [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) দেখুন। - [GitHub Issues](https://github.com/langgenius/dify/issues). Dify.AI ব্যবহার করে আপনি যেসব বাগের সম্মুখীন হন এবং ফিচার প্রস্তাবনা। আমাদের [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) দেখুন।
- [Discord](https://discord.gg/FngNHpbcY7) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম। - [Discord](https://discord.gg/FngNHpbcY7) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।
- [X(Twitter)](https://twitter.com/dify_ai) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম। - [X(Twitter)](https://twitter.com/dify_ai) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।

@ -243,7 +243,7 @@ docker compose up -d
我们欢迎您为 Dify 做出贡献,以帮助改善 Dify。包括提交代码、问题、新想法或分享您基于 Dify 创建的有趣且有用的 AI 应用程序。同时,我们也欢迎您在不同的活动、会议和社交媒体上分享 Dify。 我们欢迎您为 Dify 做出贡献,以帮助改善 Dify。包括提交代码、问题、新想法或分享您基于 Dify 创建的有趣且有用的 AI 应用程序。同时,我们也欢迎您在不同的活动、会议和社交媒体上分享 Dify。
- [Github Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。 - [GitHub Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。
- [GitHub Issues](https://github.com/langgenius/dify/issues)。👉:使用 Dify.AI 时遇到的错误和问题,请参阅[贡献指南](CONTRIBUTING.md)。 - [GitHub Issues](https://github.com/langgenius/dify/issues)。👉:使用 Dify.AI 时遇到的错误和问题,请参阅[贡献指南](CONTRIBUTING.md)。
- [电子邮件支持](mailto:hello@dify.ai?subject=[GitHub]Questions%20About%20Dify)。👉:关于使用 Dify.AI 的问题。 - [电子邮件支持](mailto:hello@dify.ai?subject=[GitHub]Questions%20About%20Dify)。👉:关于使用 Dify.AI 的问题。
- [Discord](https://discord.gg/FngNHpbcY7)。👉:分享您的应用程序并与社区交流。 - [Discord](https://discord.gg/FngNHpbcY7)。👉:分享您的应用程序并与社区交流。

@ -230,7 +230,7 @@ Falls Sie Code beitragen möchten, lesen Sie bitte unseren [Contribution Guide](
## Gemeinschaft & Kontakt ## Gemeinschaft & Kontakt
* [Github Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen. * [GitHub Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Am besten für: Fehler, auf die Sie bei der Verwendung von Dify.AI stoßen, und Funktionsvorschläge. Siehe unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [GitHub Issues](https://github.com/langgenius/dify/issues). Am besten für: Fehler, auf die Sie bei der Verwendung von Dify.AI stoßen, und Funktionsvorschläge. Siehe unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Discord](https://discord.gg/FngNHpbcY7). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community. * [Discord](https://discord.gg/FngNHpbcY7). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.
* [X(Twitter)](https://twitter.com/dify_ai). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community. * [X(Twitter)](https://twitter.com/dify_ai). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.

@ -236,7 +236,7 @@ docker compose up -d
## コミュニティ & お問い合わせ ## コミュニティ & お問い合わせ
* [Github Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。 * [GitHub Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。
* [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください * [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください
* [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。 * [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。
* [X(Twitter)](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。 * [X(Twitter)](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。

@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media
## Community & Contact ## Community & Contact
* [Github Discussion](https://github.com/langgenius/dify/discussions * [GitHub Discussion](https://github.com/langgenius/dify/discussions
). Best for: sharing feedback and asking questions. ). Best for: sharing feedback and asking questions.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).

@ -229,7 +229,7 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
## 커뮤니티 & 연락처 ## 커뮤니티 & 연락처
* [Github 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다. * [GitHub 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다.
* [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. * [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
* [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. * [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.
* [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다. * [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.

@ -229,7 +229,7 @@ Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkra
## Skupnost in stik ## Skupnost in stik
* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj. * [GitHub Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). * [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. * [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.
* [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. * [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.

@ -227,7 +227,7 @@ Aynı zamanda, lütfen Dify'ı sosyal medyada, etkinliklerde ve konferanslarda p
## Topluluk & iletişim ## Topluluk & iletişim
* [Github Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için. * [GitHub Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için.
* [GitHub Sorunları](https://github.com/langgenius/dify/issues). En uygun: Dify.AI kullanırken karşılaştığınız hatalar ve özellik önerileri için. [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakın. * [GitHub Sorunları](https://github.com/langgenius/dify/issues). En uygun: Dify.AI kullanırken karşılaştığınız hatalar ve özellik önerileri için. [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakın.
* [Discord](https://discord.gg/FngNHpbcY7). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için. * [Discord](https://discord.gg/FngNHpbcY7). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için.
* [X(Twitter)](https://twitter.com/dify_ai). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için. * [X(Twitter)](https://twitter.com/dify_ai). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için.

@ -233,7 +233,7 @@ Dify 的所有功能都提供相應的 API因此您可以輕鬆地將 Dify
## 社群與聯絡方式 ## 社群與聯絡方式
- [Github Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。 - [GitHub Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。
- [GitHub Issues](https://github.com/langgenius/dify/issues):最適合報告使用 Dify.AI 時遇到的問題和提出功能建議。請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。 - [GitHub Issues](https://github.com/langgenius/dify/issues):最適合報告使用 Dify.AI 時遇到的問題和提出功能建議。請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
- [Discord](https://discord.gg/FngNHpbcY7):最適合分享您的應用程式並與社群互動。 - [Discord](https://discord.gg/FngNHpbcY7):最適合分享您的應用程式並與社群互動。
- [X(Twitter)](https://twitter.com/dify_ai):最適合分享您的應用程式並與社群互動。 - [X(Twitter)](https://twitter.com/dify_ai):最適合分享您的應用程式並與社群互動。

@ -11,10 +11,6 @@ if TYPE_CHECKING:
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool
tenant_id: ContextVar[str] = ContextVar("tenant_id")
workflow_variable_pool: ContextVar["VariablePool"] = ContextVar("workflow_variable_pool")
""" """
To avoid race-conditions caused by gunicorn thread recycling, using RecyclableContextVar to replace with To avoid race-conditions caused by gunicorn thread recycling, using RecyclableContextVar to replace with
""" """

@ -3,7 +3,7 @@ from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.service_api import api from controllers.service_api import api
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api.wraps import validate_app_token
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from fields.annotation_fields import ( from fields.annotation_fields import (
annotation_fields, annotation_fields,
@ -14,7 +14,7 @@ from services.annotation_service import AppAnnotationService
class AnnotationReplyActionApi(Resource): class AnnotationReplyActionApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
def post(self, app_model: App, end_user: EndUser, action): def post(self, app_model: App, end_user: EndUser, action):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("score_threshold", required=True, type=float, location="json") parser.add_argument("score_threshold", required=True, type=float, location="json")
@ -31,7 +31,7 @@ class AnnotationReplyActionApi(Resource):
class AnnotationReplyActionStatusApi(Resource): class AnnotationReplyActionStatusApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) @validate_app_token
def get(self, app_model: App, end_user: EndUser, job_id, action): def get(self, app_model: App, end_user: EndUser, job_id, action):
job_id = str(job_id) job_id = str(job_id)
app_annotation_job_key = "{}_app_annotation_job_{}".format(action, str(job_id)) app_annotation_job_key = "{}_app_annotation_job_{}".format(action, str(job_id))
@ -49,7 +49,7 @@ class AnnotationReplyActionStatusApi(Resource):
class AnnotationListApi(Resource): class AnnotationListApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) @validate_app_token
def get(self, app_model: App, end_user: EndUser): def get(self, app_model: App, end_user: EndUser):
page = request.args.get("page", default=1, type=int) page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int) limit = request.args.get("limit", default=20, type=int)
@ -65,7 +65,7 @@ class AnnotationListApi(Resource):
} }
return response, 200 return response, 200
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
@marshal_with(annotation_fields) @marshal_with(annotation_fields)
def post(self, app_model: App, end_user: EndUser): def post(self, app_model: App, end_user: EndUser):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@ -77,7 +77,7 @@ class AnnotationListApi(Resource):
class AnnotationUpdateDeleteApi(Resource): class AnnotationUpdateDeleteApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) @validate_app_token
@marshal_with(annotation_fields) @marshal_with(annotation_fields)
def put(self, app_model: App, end_user: EndUser, annotation_id): def put(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor: if not current_user.is_editor:
@ -91,7 +91,7 @@ class AnnotationUpdateDeleteApi(Resource):
annotation = AppAnnotationService.update_app_annotation_directly(args, app_model.id, annotation_id) annotation = AppAnnotationService.update_app_annotation_directly(args, app_model.id, annotation_id)
return annotation return annotation
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) @validate_app_token
def delete(self, app_model: App, end_user: EndUser, annotation_id): def delete(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()

@ -99,7 +99,12 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
if user_id: if user_id:
user_id = str(user_id) user_id = str(user_id)
kwargs["end_user"] = create_or_update_end_user_for_user_id(app_model, user_id) end_user = create_or_update_end_user_for_user_id(app_model, user_id)
kwargs["end_user"] = end_user
# Set EndUser as current logged-in user for flask_login.current_user
current_app.login_manager._update_request_context_with_user(end_user) # type: ignore
user_logged_in.send(current_app._get_current_object(), user=end_user) # type: ignore
return view_func(*args, **kwargs) return view_func(*args, **kwargs)

@ -5,7 +5,7 @@ import uuid
from collections.abc import Generator, Mapping from collections.abc import Generator, Mapping
from typing import Any, Literal, Optional, Union, overload from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app from flask import Flask, copy_current_request_context, current_app, has_request_context
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@ -158,7 +158,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
trace_manager=trace_manager, trace_manager=trace_manager,
workflow_run_id=workflow_run_id, workflow_run_id=workflow_run_id,
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -240,7 +239,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
node_id=node_id, inputs=args["inputs"] node_id=node_id, inputs=args["inputs"]
), ),
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -316,7 +314,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
extras={"auto_generate_conversation_name": False}, extras={"auto_generate_conversation_name": False},
single_loop_run=AdvancedChatAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]), single_loop_run=AdvancedChatAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]),
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -399,18 +396,23 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
message_id=message.id, message_id=message.id,
) )
# new thread # new thread with request context and contextvars
worker_thread = threading.Thread( context = contextvars.copy_context()
target=self._generate_worker,
kwargs={ @copy_current_request_context
"flask_app": current_app._get_current_object(), # type: ignore def worker_with_context():
"application_generate_entity": application_generate_entity, # Run the worker within the copied context
"queue_manager": queue_manager, return context.run(
"conversation_id": conversation.id, self._generate_worker,
"message_id": message.id, flask_app=current_app._get_current_object(), # type: ignore
"context": contextvars.copy_context(), application_generate_entity=application_generate_entity,
}, queue_manager=queue_manager,
) conversation_id=conversation.id,
message_id=message.id,
context=context,
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()
@ -449,8 +451,22 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
""" """
for var, val in context.items(): for var, val in context.items():
var.set(val) var.set(val)
# Save current user before entering new app context
from flask import g
saved_user = None
if has_request_context() and hasattr(g, "_login_user"):
saved_user = g._login_user
with flask_app.app_context(): with flask_app.app_context():
try: try:
# Restore user in new app context
if saved_user is not None:
from flask import g
g._login_user = saved_user
# get conversation and message # get conversation and message
conversation = self._get_conversation(conversation_id) conversation = self._get_conversation(conversation_id)
message = self._get_message(message_id) message = self._get_message(message_id)

@ -315,6 +315,7 @@ class AdvancedChatAppGenerateTaskPipeline:
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_execution=workflow_execution, workflow_execution=workflow_execution,
) )
session.commit()
yield workflow_start_resp yield workflow_start_resp
elif isinstance( elif isinstance(

@ -5,7 +5,7 @@ import uuid
from collections.abc import Generator, Mapping from collections.abc import Generator, Mapping
from typing import Any, Literal, Union, overload from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, copy_current_request_context, current_app, has_request_context
from pydantic import ValidationError from pydantic import ValidationError
from configs import dify_config from configs import dify_config
@ -179,18 +179,23 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
message_id=message.id, message_id=message.id,
) )
# new thread # new thread with request context and contextvars
worker_thread = threading.Thread( context = contextvars.copy_context()
target=self._generate_worker,
kwargs={ @copy_current_request_context
"flask_app": current_app._get_current_object(), # type: ignore def worker_with_context():
"context": contextvars.copy_context(), # Run the worker within the copied context
"application_generate_entity": application_generate_entity, return context.run(
"queue_manager": queue_manager, self._generate_worker,
"conversation_id": conversation.id, flask_app=current_app._get_current_object(), # type: ignore
"message_id": message.id, context=context,
}, application_generate_entity=application_generate_entity,
) queue_manager=queue_manager,
conversation_id=conversation.id,
message_id=message.id,
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()
@ -227,8 +232,21 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
for var, val in context.items(): for var, val in context.items():
var.set(val) var.set(val)
# Save current user before entering new app context
from flask import g
saved_user = None
if has_request_context() and hasattr(g, "_login_user"):
saved_user = g._login_user
with flask_app.app_context(): with flask_app.app_context():
try: try:
# Restore user in new app context
if saved_user is not None:
from flask import g
g._login_user = saved_user
# get conversation and message # get conversation and message
conversation = self._get_conversation(conversation_id) conversation = self._get_conversation(conversation_id)
message = self._get_message(message_id) message = self._get_message(message_id)

@ -4,7 +4,7 @@ import uuid
from collections.abc import Generator, Mapping from collections.abc import Generator, Mapping
from typing import Any, Literal, Union, overload from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, copy_current_request_context, current_app
from pydantic import ValidationError from pydantic import ValidationError
from configs import dify_config from configs import dify_config
@ -170,17 +170,18 @@ class ChatAppGenerator(MessageBasedAppGenerator):
message_id=message.id, message_id=message.id,
) )
# new thread # new thread with request context
worker_thread = threading.Thread( @copy_current_request_context
target=self._generate_worker, def worker_with_context():
kwargs={ return self._generate_worker(
"flask_app": current_app._get_current_object(), # type: ignore flask_app=current_app._get_current_object(), # type: ignore
"application_generate_entity": application_generate_entity, application_generate_entity=application_generate_entity,
"queue_manager": queue_manager, queue_manager=queue_manager,
"conversation_id": conversation.id, conversation_id=conversation.id,
"message_id": message.id, message_id=message.id,
}, )
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()

@ -4,7 +4,7 @@ import uuid
from collections.abc import Generator, Mapping from collections.abc import Generator, Mapping
from typing import Any, Literal, Union, overload from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, copy_current_request_context, current_app
from pydantic import ValidationError from pydantic import ValidationError
from configs import dify_config from configs import dify_config
@ -151,16 +151,17 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
message_id=message.id, message_id=message.id,
) )
# new thread # new thread with request context
worker_thread = threading.Thread( @copy_current_request_context
target=self._generate_worker, def worker_with_context():
kwargs={ return self._generate_worker(
"flask_app": current_app._get_current_object(), # type: ignore flask_app=current_app._get_current_object(), # type: ignore
"application_generate_entity": application_generate_entity, application_generate_entity=application_generate_entity,
"queue_manager": queue_manager, queue_manager=queue_manager,
"message_id": message.id, message_id=message.id,
}, )
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()
@ -313,16 +314,17 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
message_id=message.id, message_id=message.id,
) )
# new thread # new thread with request context
worker_thread = threading.Thread( @copy_current_request_context
target=self._generate_worker, def worker_with_context():
kwargs={ return self._generate_worker(
"flask_app": current_app._get_current_object(), # type: ignore flask_app=current_app._get_current_object(), # type: ignore
"application_generate_entity": application_generate_entity, application_generate_entity=application_generate_entity,
"queue_manager": queue_manager, queue_manager=queue_manager,
"message_id": message.id, message_id=message.id,
}, )
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()

@ -5,7 +5,7 @@ import uuid
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import Any, Literal, Optional, Union, overload from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app from flask import Flask, copy_current_request_context, current_app, has_request_context
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@ -135,7 +135,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
workflow_run_id=workflow_run_id, workflow_run_id=workflow_run_id,
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -207,17 +206,22 @@ class WorkflowAppGenerator(BaseAppGenerator):
app_mode=app_model.mode, app_mode=app_model.mode,
) )
# new thread # new thread with request context and contextvars
worker_thread = threading.Thread( context = contextvars.copy_context()
target=self._generate_worker,
kwargs={ @copy_current_request_context
"flask_app": current_app._get_current_object(), # type: ignore def worker_with_context():
"application_generate_entity": application_generate_entity, # Run the worker within the copied context
"queue_manager": queue_manager, return context.run(
"context": contextvars.copy_context(), self._generate_worker,
"workflow_thread_pool_id": workflow_thread_pool_id, flask_app=current_app._get_current_object(), # type: ignore
}, application_generate_entity=application_generate_entity,
) queue_manager=queue_manager,
context=context,
workflow_thread_pool_id=workflow_thread_pool_id,
)
worker_thread = threading.Thread(target=worker_with_context)
worker_thread.start() worker_thread.start()
@ -277,7 +281,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
), ),
workflow_run_id=str(uuid.uuid4()), workflow_run_id=str(uuid.uuid4()),
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -354,7 +357,6 @@ class WorkflowAppGenerator(BaseAppGenerator):
single_loop_run=WorkflowAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]), single_loop_run=WorkflowAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args["inputs"]),
workflow_run_id=str(uuid.uuid4()), workflow_run_id=str(uuid.uuid4()),
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
contexts.plugin_tool_providers.set({}) contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock()) contexts.plugin_tool_providers_lock.set(threading.Lock())
@ -408,8 +410,22 @@ class WorkflowAppGenerator(BaseAppGenerator):
""" """
for var, val in context.items(): for var, val in context.items():
var.set(val) var.set(val)
# Save current user before entering new app context
from flask import g
saved_user = None
if has_request_context() and hasattr(g, "_login_user"):
saved_user = g._login_user
with flask_app.app_context(): with flask_app.app_context():
try: try:
# Restore user in new app context
if saved_user is not None:
from flask import g
g._login_user = saved_user
# workflow app # workflow app
runner = WorkflowAppRunner( runner = WorkflowAppRunner(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,

@ -51,15 +51,19 @@ class LLMGenerator:
response = cast( response = cast(
LLMResult, LLMResult,
model_instance.invoke_llm( model_instance.invoke_llm(
prompt_messages=list(prompts), model_parameters={"max_tokens": 100, "temperature": 1}, stream=False prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False
), ),
) )
answer = cast(str, response.message.content) answer = cast(str, response.message.content)
cleaned_answer = re.sub(r"^.*(\{.*\}).*$", r"\1", answer, flags=re.DOTALL) cleaned_answer = re.sub(r"^.*(\{.*\}).*$", r"\1", answer, flags=re.DOTALL)
if cleaned_answer is None: if cleaned_answer is None:
return "" return ""
result_dict = json.loads(cleaned_answer) try:
answer = result_dict["Your Output"] result_dict = json.loads(cleaned_answer)
answer = result_dict["Your Output"]
except json.JSONDecodeError as e:
logging.exception("Failed to generate name after answer, use query instead")
answer = query
name = answer.strip() name = answer.strip()
if len(name) > 75: if len(name) > 75:

@ -366,7 +366,7 @@ def _extract_text_from_excel(file_content: bytes) -> str:
df = excel_file.parse(sheet_name=sheet_name) df = excel_file.parse(sheet_name=sheet_name)
df.dropna(how="all", inplace=True) df.dropna(how="all", inplace=True)
# Create Markdown table two times to separate tables with a newline # Create Markdown table two times to separate tables with a newline
markdown_table += df.to_markdown(index=False) + "\n\n" markdown_table += df.to_markdown(index=False, floatfmt="") + "\n\n"
except Exception as e: except Exception as e:
continue continue
return markdown_table return markdown_table

@ -5,11 +5,11 @@ from flask import Response, request
from flask_login import user_loaded_from_request, user_logged_in from flask_login import user_loaded_from_request, user_logged_in
from werkzeug.exceptions import NotFound, Unauthorized from werkzeug.exceptions import NotFound, Unauthorized
import contexts from configs import dify_config
from dify_app import DifyApp from dify_app import DifyApp
from extensions.ext_database import db from extensions.ext_database import db
from libs.passport import PassportService from libs.passport import PassportService
from models.account import Account from models.account import Account, Tenant, TenantAccountJoin
from models.model import EndUser from models.model import EndUser
from services.account_service import AccountService from services.account_service import AccountService
@ -32,6 +32,26 @@ def load_user_from_request(request_from_flask_login):
else: else:
auth_token = request.args.get("_token") auth_token = request.args.get("_token")
# Check for admin API key authentication first
if dify_config.ADMIN_API_KEY_ENABLE and auth_header:
admin_api_key = dify_config.ADMIN_API_KEY
if admin_api_key and admin_api_key == auth_token:
workspace_id = request.headers.get("X-WORKSPACE-ID")
if workspace_id:
tenant_account_join = (
db.session.query(Tenant, TenantAccountJoin)
.filter(Tenant.id == workspace_id)
.filter(TenantAccountJoin.tenant_id == Tenant.id)
.filter(TenantAccountJoin.role == "owner")
.one_or_none()
)
if tenant_account_join:
tenant, ta = tenant_account_join
account = db.session.query(Account).filter_by(id=ta.account_id).first()
if account:
account.current_tenant = tenant
return account
if request.blueprint in {"console", "inner_api"}: if request.blueprint in {"console", "inner_api"}:
if not auth_token: if not auth_token:
raise Unauthorized("Invalid Authorization token.") raise Unauthorized("Invalid Authorization token.")
@ -61,8 +81,8 @@ def on_user_logged_in(_sender, user):
Note: AccountService.load_logged_in_account will populate user.current_tenant_id Note: AccountService.load_logged_in_account will populate user.current_tenant_id
through the load_user method, which calls account.set_tenant_id(). through the load_user method, which calls account.set_tenant_id().
""" """
if user and isinstance(user, Account) and user.current_tenant_id: # tenant_id context variable removed - using current_user.current_tenant_id directly
contexts.tenant_id.set(user.current_tenant_id) pass
@login_manager.unauthorized_handler @login_manager.unauthorized_handler

@ -100,6 +100,8 @@ app_partial_fields = {
"updated_at": TimestampField, "updated_at": TimestampField,
"tags": fields.List(fields.Nested(tag_fields)), "tags": fields.List(fields.Nested(tag_fields)),
"access_mode": fields.String, "access_mode": fields.String,
"create_user_name": fields.String,
"author_name": fields.String,
} }

@ -2,14 +2,11 @@ from functools import wraps
from typing import Any from typing import Any
from flask import current_app, g, has_request_context, request from flask import current_app, g, has_request_context, request
from flask_login import user_logged_in # type: ignore
from flask_login.config import EXEMPT_METHODS # type: ignore from flask_login.config import EXEMPT_METHODS # type: ignore
from werkzeug.exceptions import Unauthorized
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from configs import dify_config from configs import dify_config
from extensions.ext_database import db from models.account import Account
from models.account import Account, Tenant, TenantAccountJoin
from models.model import EndUser from models.model import EndUser
#: A proxy for the current user. If no user is logged in, this will be an #: A proxy for the current user. If no user is logged in, this will be an
@ -53,36 +50,6 @@ def login_required(func):
@wraps(func) @wraps(func)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
auth_header = request.headers.get("Authorization")
if dify_config.ADMIN_API_KEY_ENABLE:
if auth_header:
if " " not in auth_header:
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
admin_api_key = dify_config.ADMIN_API_KEY
if admin_api_key:
if admin_api_key == auth_token:
workspace_id = request.headers.get("X-WORKSPACE-ID")
if workspace_id:
tenant_account_join = (
db.session.query(Tenant, TenantAccountJoin)
.filter(Tenant.id == workspace_id)
.filter(TenantAccountJoin.tenant_id == Tenant.id)
.filter(TenantAccountJoin.role == "owner")
.one_or_none()
)
if tenant_account_join:
tenant, ta = tenant_account_join
account = db.session.query(Account).filter_by(id=ta.account_id).first()
# Login admin
if account:
account.current_tenant = tenant
current_app.login_manager._update_request_context_with_user(account) # type: ignore
user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore
if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED: if request.method in EXEMPT_METHODS or dify_config.LOGIN_DISABLED:
pass pass
elif not current_user.is_authenticated: elif not current_user.is_authenticated:

@ -294,6 +294,15 @@ class App(Base):
return tags or [] return tags or []
@property
def author_name(self):
if self.created_by:
account = db.session.query(Account).filter(Account.id == self.created_by).first()
if account:
return account.name
return None
class AppModelConfig(Base): class AppModelConfig(Base):
__tablename__ = "app_model_configs" __tablename__ = "app_model_configs"

@ -6,6 +6,8 @@ from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Any, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union
from uuid import uuid4 from uuid import uuid4
from flask_login import current_user
from core.variables import utils as variable_utils from core.variables import utils as variable_utils
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
from factories.variable_factory import build_segment from factories.variable_factory import build_segment
@ -17,7 +19,6 @@ import sqlalchemy as sa
from sqlalchemy import UniqueConstraint, func from sqlalchemy import UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
import contexts
from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
from core.helper import encrypter from core.helper import encrypter
from core.variables import SecretVariable, Segment, SegmentType, Variable from core.variables import SecretVariable, Segment, SegmentType, Variable
@ -274,7 +275,16 @@ class Workflow(Base):
if self._environment_variables is None: if self._environment_variables is None:
self._environment_variables = "{}" self._environment_variables = "{}"
tenant_id = contexts.tenant_id.get() # Get tenant_id from current_user (Account or EndUser)
if isinstance(current_user, Account):
# Account user
tenant_id = current_user.current_tenant_id
else:
# EndUser
tenant_id = current_user.tenant_id
if not tenant_id:
return []
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables) environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
results = [ results = [
@ -297,7 +307,17 @@ class Workflow(Base):
self._environment_variables = "{}" self._environment_variables = "{}"
return return
tenant_id = contexts.tenant_id.get() # Get tenant_id from current_user (Account or EndUser)
if isinstance(current_user, Account):
# Account user
tenant_id = current_user.current_tenant_id
else:
# EndUser
tenant_id = current_user.tenant_id
if not tenant_id:
self._environment_variables = "{}"
return
value = list(value) value = list(value)
if any(var for var in value if not var.id): if any(var for var in value if not var.id):

@ -10,6 +10,7 @@ from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.nodes.document_extractor import DocumentExtractorNode, DocumentExtractorNodeData from core.workflow.nodes.document_extractor import DocumentExtractorNode, DocumentExtractorNodeData
from core.workflow.nodes.document_extractor.node import ( from core.workflow.nodes.document_extractor.node import (
_extract_text_from_docx, _extract_text_from_docx,
_extract_text_from_excel,
_extract_text_from_pdf, _extract_text_from_pdf,
_extract_text_from_plain_text, _extract_text_from_plain_text,
) )
@ -182,3 +183,181 @@ def test_extract_text_from_docx(mock_document):
def test_node_type(document_extractor_node): def test_node_type(document_extractor_node):
assert document_extractor_node._node_type == NodeType.DOCUMENT_EXTRACTOR assert document_extractor_node._node_type == NodeType.DOCUMENT_EXTRACTOR
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_single_sheet(mock_excel_file):
"""Test extracting text from Excel file with single sheet."""
# Mock DataFrame
mock_df = Mock()
mock_df.dropna = Mock()
mock_df.to_markdown.return_value = "| Name | Age |\n|------|-----|\n| John | 25 |"
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["Sheet1"]
mock_excel_instance.parse.return_value = mock_df
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_content"
result = _extract_text_from_excel(file_content)
expected = "| Name | Age |\n|------|-----|\n| John | 25 |\n\n"
assert result == expected
mock_excel_file.assert_called_once()
mock_df.dropna.assert_called_once_with(how="all", inplace=True)
mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="")
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_multiple_sheets(mock_excel_file):
"""Test extracting text from Excel file with multiple sheets."""
# Mock DataFrames for different sheets
mock_df1 = Mock()
mock_df1.dropna = Mock()
mock_df1.to_markdown.return_value = "| Product | Price |\n|---------|-------|\n| Apple | 1.50 |"
mock_df2 = Mock()
mock_df2.dropna = Mock()
mock_df2.to_markdown.return_value = "| City | Population |\n|------|------------|\n| NYC | 8000000 |"
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["Products", "Cities"]
mock_excel_instance.parse.side_effect = [mock_df1, mock_df2]
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_content_multiple_sheets"
result = _extract_text_from_excel(file_content)
expected = (
"| Product | Price |\n|---------|-------|\n| Apple | 1.50 |\n\n"
"| City | Population |\n|------|------------|\n| NYC | 8000000 |\n\n"
)
assert result == expected
assert mock_excel_instance.parse.call_count == 2
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_empty_sheets(mock_excel_file):
"""Test extracting text from Excel file with empty sheets."""
# Mock empty DataFrame
mock_df = Mock()
mock_df.dropna = Mock()
mock_df.to_markdown.return_value = ""
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["EmptySheet"]
mock_excel_instance.parse.return_value = mock_df
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_empty_content"
result = _extract_text_from_excel(file_content)
expected = "\n\n"
assert result == expected
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_sheet_parse_error(mock_excel_file):
"""Test handling of sheet parsing errors - should continue with other sheets."""
# Mock DataFrames - one successful, one that raises exception
mock_df_success = Mock()
mock_df_success.dropna = Mock()
mock_df_success.to_markdown.return_value = "| Data | Value |\n|------|-------|\n| Test | 123 |"
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["GoodSheet", "BadSheet"]
mock_excel_instance.parse.side_effect = [mock_df_success, Exception("Parse error")]
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_mixed_content"
result = _extract_text_from_excel(file_content)
expected = "| Data | Value |\n|------|-------|\n| Test | 123 |\n\n"
assert result == expected
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_file_error(mock_excel_file):
"""Test handling of Excel file reading errors."""
mock_excel_file.side_effect = Exception("Invalid Excel file")
file_content = b"invalid_excel_content"
with pytest.raises(Exception) as exc_info:
_extract_text_from_excel(file_content)
# Note: The function should raise TextExtractionError, but since it's not imported in the test,
# we check for the general Exception pattern
assert "Failed to extract text from Excel file" in str(exc_info.value)
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_io_bytesio_usage(mock_excel_file):
"""Test that BytesIO is properly used with the file content."""
import io
# Mock DataFrame
mock_df = Mock()
mock_df.dropna = Mock()
mock_df.to_markdown.return_value = "| Test | Data |\n|------|------|\n| 1 | A |"
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["TestSheet"]
mock_excel_instance.parse.return_value = mock_df
mock_excel_file.return_value = mock_excel_instance
file_content = b"test_excel_bytes"
result = _extract_text_from_excel(file_content)
# Verify that ExcelFile was called with a BytesIO object
mock_excel_file.assert_called_once()
call_args = mock_excel_file.call_args[0][0]
assert isinstance(call_args, io.BytesIO)
expected = "| Test | Data |\n|------|------|\n| 1 | A |\n\n"
assert result == expected
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_all_sheets_fail(mock_excel_file):
"""Test when all sheets fail to parse - should return empty string."""
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["BadSheet1", "BadSheet2"]
mock_excel_instance.parse.side_effect = [Exception("Error 1"), Exception("Error 2")]
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_all_bad_sheets"
result = _extract_text_from_excel(file_content)
# Should return empty string when all sheets fail
assert result == ""
@patch("pandas.ExcelFile")
def test_extract_text_from_excel_markdown_formatting(mock_excel_file):
"""Test that markdown formatting parameters are correctly applied."""
# Mock DataFrame
mock_df = Mock()
mock_df.dropna = Mock()
mock_df.to_markdown.return_value = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |"
# Mock ExcelFile
mock_excel_instance = Mock()
mock_excel_instance.sheet_names = ["NumberSheet"]
mock_excel_instance.parse.return_value = mock_df
mock_excel_file.return_value = mock_excel_instance
file_content = b"fake_excel_numbers"
result = _extract_text_from_excel(file_content)
# Verify to_markdown was called with correct parameters
mock_df.to_markdown.assert_called_once_with(index=False, floatfmt="")
expected = "| Float | Int |\n|-------|-----|\n| 123456.78 | 42 |\n\n"
assert result == expected

@ -2,14 +2,13 @@ import json
from unittest import mock from unittest import mock
from uuid import uuid4 from uuid import uuid4
import contexts
from constants import HIDDEN_VALUE from constants import HIDDEN_VALUE
from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable
from models.workflow import Workflow, WorkflowNodeExecution from models.workflow import Workflow, WorkflowNodeExecution
def test_environment_variables(): def test_environment_variables():
contexts.tenant_id.set("tenant_id") # tenant_id context variable removed - using current_user.current_tenant_id directly
# Create a Workflow instance # Create a Workflow instance
workflow = Workflow( workflow = Workflow(
@ -38,9 +37,14 @@ def test_environment_variables():
{"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]} {"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]}
) )
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user.tenant_id = "tenant_id"
with ( with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
): ):
# Set the environment_variables property of the Workflow instance # Set the environment_variables property of the Workflow instance
variables = [variable1, variable2, variable3, variable4] variables = [variable1, variable2, variable3, variable4]
@ -51,7 +55,7 @@ def test_environment_variables():
def test_update_environment_variables(): def test_update_environment_variables():
contexts.tenant_id.set("tenant_id") # tenant_id context variable removed - using current_user.current_tenant_id directly
# Create a Workflow instance # Create a Workflow instance
workflow = Workflow( workflow = Workflow(
@ -80,9 +84,14 @@ def test_update_environment_variables():
{"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]} {"name": "var4", "value": 3.14, "id": str(uuid4()), "selector": ["env", "var4"]}
) )
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user.tenant_id = "tenant_id"
with ( with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
): ):
variables = [variable1, variable2, variable3, variable4] variables = [variable1, variable2, variable3, variable4]
@ -104,7 +113,7 @@ def test_update_environment_variables():
def test_to_dict(): def test_to_dict():
contexts.tenant_id.set("tenant_id") # tenant_id context variable removed - using current_user.current_tenant_id directly
# Create a Workflow instance # Create a Workflow instance
workflow = Workflow( workflow = Workflow(
@ -121,9 +130,14 @@ def test_to_dict():
# Create some EnvironmentVariable instances # Create some EnvironmentVariable instances
# Mock current_user as an EndUser
mock_user = mock.Mock()
mock_user.tenant_id = "tenant_id"
with ( with (
mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"), mock.patch("core.helper.encrypter.encrypt_token", return_value="encrypted_token"),
mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"), mock.patch("core.helper.encrypter.decrypt_token", return_value="secret"),
mock.patch("models.workflow.current_user", mock_user),
): ):
# Set the environment_variables property of the Workflow instance # Set the environment_variables property of the Workflow instance
workflow.environment_variables = [ workflow.environment_variables = [

@ -2,7 +2,7 @@
import { useContext, useContextSelector } from 'use-context-selector' import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -35,6 +35,7 @@ import Tooltip from '@/app/components/base/tooltip'
import AccessControl from '@/app/components/app/app-access-control' import AccessControl from '@/app/components/app/app-access-control'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { formatTime } from '@/utils/time'
export type AppCardProps = { export type AppCardProps = {
app: App app: App
@ -296,6 +297,15 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setTags(app.tags) setTags(app.tags)
}, [app.tags]) }, [app.tags])
const EditTimeText = useMemo(() => {
const timeText = formatTime({
date: (app.updated_at || app.created_at) * 1000,
dateFormat: 'MM/DD/YYYY h:mm',
})
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app.updated_at, app.created_at])
return ( return (
<> <>
<div <div
@ -320,12 +330,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'> <div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={app.name}>{app.name}</div> <div className='truncate' title={app.name}>{app.name}</div>
</div> </div>
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'> <div className='flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary'>
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>} <div className='truncate' title={app.author_name}>{app.author_name}</div>
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>} <div>·</div>
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>} <div className='truncate'>{EditTimeText}</div>
{app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div> </div>
</div> </div>
<div className='flex h-5 w-5 shrink-0 items-center justify-center'> <div className='flex h-5 w-5 shrink-0 items-center justify-center'>

@ -32,7 +32,6 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item' import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import MessageLogModal from '@/app/components/base/message-log-modal' import MessageLogModal from '@/app/components/base/message-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
@ -191,13 +190,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext() const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp() const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext) const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem, currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem, setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal, showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal, setShowMessageLogModal: state.setShowMessageLogModal,
showPromptLogModal: state.showPromptLogModal,
setShowPromptLogModal: state.setShowPromptLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab, currentLogModalActiveTab: state.currentLogModalActiveTab,
}))) })))
const { t } = useTranslation() const { t } = useTranslation()
@ -518,16 +515,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
defaultTab={currentLogModalActiveTab} defaultTab={currentLogModalActiveTab}
/> />
)} )}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
</div> </div>
) )
} }

@ -234,6 +234,4 @@ const Answer: FC<AnswerProps> = ({
) )
} }
export default memo(Answer, (prevProps, nextProps) => export default memo(Answer)
prevProps.responding === false && nextProps.responding === false,
)

@ -91,6 +91,11 @@ const initMermaid = () => {
numberSectionStyles: 4, numberSectionStyles: 4,
axisFormat: '%Y-%m-%d', axisFormat: '%Y-%m-%d',
}, },
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
maxTextSize: 50000, maxTextSize: 50000,
}) })
isMermaidInitialized = true isMermaidInitialized = true
@ -289,11 +294,12 @@ const Flowchart = React.forwardRef((props: {
try { try {
let finalCode: string let finalCode: string
// Check if it's a gantt chart // Check if it's a gantt chart or mindmap
const isGanttChart = primitiveCode.trim().startsWith('gantt') const isGanttChart = primitiveCode.trim().startsWith('gantt')
const isMindMap = primitiveCode.trim().startsWith('mindmap')
if (isGanttChart) { if (isGanttChart || isMindMap) {
// For gantt charts, ensure each task is on its own line // For gantt charts and mindmaps, ensure each task is on its own line
// and preserve exact whitespace/format // and preserve exact whitespace/format
finalCode = primitiveCode.trim() finalCode = primitiveCode.trim()
} }
@ -352,6 +358,11 @@ const Flowchart = React.forwardRef((props: {
numberSectionStyles: 4, numberSectionStyles: 4,
axisFormat: '%Y-%m-%d', axisFormat: '%Y-%m-%d',
}, },
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
} }
if (look === 'classic') { if (look === 'classic') {
@ -476,15 +487,15 @@ const Flowchart = React.forwardRef((props: {
'bg-white': currentTheme === Theme.light, 'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark, 'bg-slate-900': currentTheme === Theme.dark,
}), }),
mermaidDiv: cn('mermaid relative h-auto w-full cursor-pointer', { mermaidDiv: cn('mermaid cursor-pointer h-auto w-full relative', {
'bg-white': currentTheme === Theme.light, 'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark, 'bg-slate-900': currentTheme === Theme.dark,
}), }),
errorMessage: cn('px-[26px] py-4', { errorMessage: cn('py-4 px-[26px]', {
'text-red-500': currentTheme === Theme.light, 'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark, 'text-red-400': currentTheme === Theme.dark,
}), }),
errorIcon: cn('h-6 w-6', { errorIcon: cn('w-6 h-6', {
'text-red-500': currentTheme === Theme.light, 'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark, 'text-red-400': currentTheme === Theme.dark,
}), }),
@ -492,7 +503,7 @@ const Flowchart = React.forwardRef((props: {
'text-gray-700': currentTheme === Theme.light, 'text-gray-700': currentTheme === Theme.light,
'text-gray-300': currentTheme === Theme.dark, 'text-gray-300': currentTheme === Theme.dark,
}), }),
themeToggle: cn('flex h-10 w-10 items-center justify-center rounded-full shadow-md backdrop-blur-sm transition-all duration-300', { themeToggle: cn('flex items-center justify-center w-10 h-10 rounded-full transition-all duration-300 shadow-md backdrop-blur-sm', {
'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light, 'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light,
'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark, 'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark,
}), }),
@ -501,7 +512,7 @@ const Flowchart = React.forwardRef((props: {
// Style classes for look options // Style classes for look options
const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => { const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => {
return cn( return cn(
'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary', 'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary', look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300', currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300',
look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white', look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white',
@ -512,7 +523,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}> <div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}>
<div className={themeClasses.segmented}> <div className={themeClasses.segmented}>
<div className="msh-segmented-group"> <div className="msh-segmented-group">
<label className="msh-segmented-item m-2 flex w-[200px] items-center space-x-1"> <label className="msh-segmented-item flex items-center space-x-1 m-2 w-[200px]">
<div <div
key='classic' key='classic'
className={getLookButtonClass('classic')} className={getLookButtonClass('classic')}
@ -534,7 +545,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} /> <div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} />
{isLoading && !svgCode && ( {isLoading && !svgCode && (
<div className='px-[26px] py-4'> <div className='py-4 px-[26px]'>
<LoadingAnim type='text'/> <LoadingAnim type='text'/>
{!isCodeComplete && ( {!isCodeComplete && (
<div className="mt-2 text-sm text-gray-500"> <div className="mt-2 text-sm text-gray-500">
@ -546,7 +557,7 @@ const Flowchart = React.forwardRef((props: {
{svgCode && ( {svgCode && (
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}> <div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}>
<div className="absolute bottom-2 left-2 z-[100]"> <div className="absolute left-2 bottom-2 z-[100]">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()

@ -22,6 +22,10 @@ export function preprocessMermaidCode(code: string): string {
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`) .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`)
// Fix common syntax issues // Fix common syntax issues
.replace(/fifopacket/g, 'rect') .replace(/fifopacket/g, 'rect')
// Ensure graph has direction
.replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
return direction ? match : 'graph TD'
})
// Clean up empty lines and extra spaces // Clean up empty lines and extra spaces
.trim() .trim()
} }
@ -32,9 +36,9 @@ export function preprocessMermaidCode(code: string): string {
export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string { export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
let finalCode = preprocessMermaidCode(code) let finalCode = preprocessMermaidCode(code)
// Special handling for gantt charts // Special handling for gantt charts and mindmaps
if (finalCode.trim().startsWith('gantt')) { if (finalCode.trim().startsWith('gantt') || finalCode.trim().startsWith('mindmap')) {
// For gantt charts, preserve the structure exactly as is // For gantt charts and mindmaps, preserve the structure exactly as is
return finalCode return finalCode
} }
@ -173,8 +177,15 @@ export function isMermaidCodeComplete(code: string): boolean {
return lines.length >= 3 return lines.length >= 3
} }
// Special handling for mindmaps
if (trimmedCode.startsWith('mindmap')) {
// For mindmaps, check if it has at least a root node
const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
return lines.length >= 2
}
// Check for basic syntax structure // Check for basic syntax structure
const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram)/.test(trimmedCode) const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram|mindmap)/.test(trimmedCode)
// Check for balanced brackets and parentheses // Check for balanced brackets and parentheses
const isBalanced = (() => { const isBalanced = (() => {

@ -91,6 +91,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
customComponent: isFullDocMode && CustomButton, customComponent: isFullDocMode && CustomButton,
}) })
handleCancel('add') handleCancel('add')
setContent('')
if (isFullDocMode) { if (isFullDocMode) {
refreshTimer.current = setTimeout(() => { refreshTimer.current = setTimeout(() => {
onSave() onSave()

@ -118,6 +118,9 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
customComponent: CustomButton, customComponent: CustomButton,
}) })
handleCancel('add') handleCancel('add')
setQuestion('')
setAnswer('')
setKeywords([])
refreshTimer.current = setTimeout(() => { refreshTimer.current = setTimeout(() => {
onSave() onSave()
}, 3000) }, 3000)

@ -111,7 +111,7 @@ const DatasetCard = ({
return ( return (
<> <>
<div <div
className='group relative col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg' className='group relative col-span-1 flex min-h-[171px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
data-disable-nprogress={true} data-disable-nprogress={true}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()

@ -121,7 +121,7 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
} }
} }
// common params // common params
if (param.required && !payload.agent_parameters?.[param.name]?.value) { if (param.required && !(payload.agent_parameters?.[param.name]?.value || param.default)) {
return { return {
isValid: false, isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }), errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),

@ -316,6 +316,8 @@ export type App = {
name: string name: string
/** Description */ /** Description */
description: string description: string
/** Author Name */
author_name: string;
/** /**
* Icon Type * Icon Type
@ -348,6 +350,8 @@ export type App = {
app_model_config: ModelConfig app_model_config: ModelConfig
/** Timestamp of creation */ /** Timestamp of creation */
created_at: number created_at: number
/** Timestamp of update */
updated_at: number
/** Web Application Configuration */ /** Web Application Configuration */
site: SiteConfig site: SiteConfig
/** api site url */ /** api site url */

Loading…
Cancel
Save