Compare commits

...

79 Commits

Author SHA1 Message Date
Junyan Qin a0c0d0fcd5
doc: add debugging steps in api/README.md 9 months ago
Joel 6d826d4dc6 fix: not from markpalceplace show auto update 9 months ago
Junyan Qin 7be548a694
chore: remove volumns field in docker-compose-template.yaml for worker_beat service 9 months ago
Junyan Qin 3a9ef9fd47
chore: remove volumn field in worker_beat service 9 months ago
Junyan Qin 0b41b84adf
chore: english comments 9 months ago
Joel 6c515bf9c3
Update web/app/components/plugins/plugin-detail-panel/detail-header.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
9 months ago
Joel a9a6e773a1
Update web/app/components/base/date-and-time-picker/utils/dayjs.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
9 months ago
Joel ebec1cf2d8
Update web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
9 months ago
Joel ed5a5962d2
Update web/app/components/plugins/reference-setting-modal/auto-update-setting/utils.spec.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
9 months ago
Joel 6e8a3c8021 chore: lint error 9 months ago
Joel 93890e6658 chore: add icon json \n 9 months ago
crazywoola c62e8bf71e chore: add icons 9 months ago
Joel 2d8f6f4a48 chore: fix: picker always show 9 months ago
Junyan Qin 2d960ce401
feat: resp DISABLED for exists users 9 months ago
Junyan Qin a519a7c50c
fix: db migration 9 months ago
Junyan Qin 5e7a7cc0c7
feat: create default autoupgrade strategy on tenant creating 9 months ago
Junyan Qin 6a29b9f766
chore: add worker_beat to docker compose template 9 months ago
Junyan Qin 1016678ea4
chore: add scheduler tasks switch in docker .env 9 months ago
Junyan Qin a2f64e23c9
chore: add scheduler tasks switch in docker .env 9 months ago
Junyan Qin 5d722c19a7
feat: add switch to config celery schedule tasks 9 months ago
Joel ad40295b75
feat: handle downgrade install 9 months ago
Joel 22d0fadcd0
chore: add auto update show config 9 months ago
Joel 31976996f8
feat: select box setting 9 months ago
Joel c919823000
feat: downgrade modal 9 months ago
Joel 23a9ad23ae
feat: auto update button 9 months ago
Junyan Qin 5b0bbe7a3b
ci: add `build/**` branch to build and push ci 9 months ago
Junyan Qin 2523f5870a
fix: incorrect down_revision in db migration 9 months ago
Junyan Qin 23a5dc3e32
fix: ruff format 9 months ago
Junyan Qin 40feb607c1
fix: type static check errors 9 months ago
Junyan Qin 74d61fda2a
fix: ruff format 9 months ago
Junyan Qin 2c795ec301
feat: add plugin queue to celery cmd in entrypoint.sh 9 months ago
Junyan Qin bcfbeee333
perf: split tasks to multi worker 9 months ago
Junyan Qin 6674d7fc18
feat: exclude one plugin 9 months ago
Junyan Qin f373e3df99
feat: add supports for `update_all` strategy 9 months ago
Junyan Qin 60bce19696
feat: combine plugin preferences apis 9 months ago
Junyan Qin c2520f7cb4
fix: bugs 9 months ago
Junyan Qin 8b62e5520a
feat(auto-upgrade): celery scheduled task 9 months ago
Junyan Qin 71b3d6ad9c
feat: crud for auto upgrade strategy 9 months ago
RockChinQ fccb00c281
feat(auto upgrade): add upgrade setting 9 months ago
湛露先生 769b43cc3b
update worklow events logs. (#19871)
Signed-off-by: zhanluxianshen <zhanluxianshen@163.com>
9 months ago
Joel 7608eb1049 Merge branch 'main' into feat/plugin-auto-upgrade-fe 9 months ago
Joel 95ce7b6f47 feat: add time zone 9 months ago
Joel 784a236280 Merge branch 'main' into feat/plugin-auto-upgrade-fe 9 months ago
Joel 1e0426ca6f chore: peroid not auto scroll 9 months ago
Joel fd7396d8f9 chore: icon fixed 9 months ago
Joel a0af33e945 Merge branch 'main' into feat/plugin-auto-upgrade-fe 9 months ago
Joel 8d8220b06c fix: utc time show 9 months ago
Joel 0625d6a361 fix: not use local time 9 months ago
Joel 63a1a1077e Merge branch 'main' into feat/plugin-auto-upgrade-fe 9 months ago
Joel 0af646d947 fix: fetch installed plugin instead of all plugins 9 months ago
Joel 07c99745fa feat: handle downgrade install 9 months ago
Joel afd0d31354 fix: not the same as 9 months ago
Joel 18bbf1165d feat: exculde call api 9 months ago
Joel 5f17edc77f feat: downgrade detect 9 months ago
Joel 836027cb33 chore: add auto update show config 9 months ago
Joel f3cbfe2223 feat: config can save 9 months ago
Joel bc1e4c88e0 feat: no data placeholder 9 months ago
Joel d114485abd feat: pluging loading 9 months ago
Joel 3e8a4a66fe feat: api to refernce settings 9 months ago
Joel 4c583f3d9a feat: can select plugins 9 months ago
Joel 52b845a5bb feat: select box setting 9 months ago
Joel 38d1c85c57 main 9 months ago
Joel c43d992f2b feat: fetch plugin list 9 months ago
Joel 1ff5969b92 feat: select tool template 9 months ago
Joel 93a560ee54 chore: ui and clear 9 months ago
Joel 2f241d932c chore: temp i18n 9 months ago
Joel a0804786fd feat: downgrade modal i18n 9 months ago
Joel c6fa8102eb feat: downgrade modal 9 months ago
Joel 7ec5816513 feat: show downgrade warning logic 9 months ago
Joel 825fbcc6f8 feat: auto update button 9 months ago
Joel ccef71626d feat: show list and select 9 months ago
Joel 29cac85b12 feat: plugin no data 9 months ago
Joel 8b290ac7a1 feat: only choose 15 time 9 months ago
Joel 01cdffaa08 feat: plugins picker holder 9 months ago
Joel 3061280f7a fat: auto update mode 9 months ago
Joel bc75d810c4 feat: choose time 9 months ago
Joel dc5e974a78 feat: choose auto update description and i18n 9 months ago
Joel baff25c160 feat: auto update strategy picker 9 months ago
Joel 42b6524954 feat: type config 10 months ago

@ -6,6 +6,7 @@ on:
- "main" - "main"
- "deploy/dev" - "deploy/dev"
- "deploy/enterprise" - "deploy/enterprise"
- "build/**"
tags: tags:
- "*" - "*"

@ -451,6 +451,16 @@ APP_MAX_ACTIVE_REQUESTS=0
# Celery beat configuration # Celery beat configuration
CELERY_BEAT_SCHEDULER_TIME=1 CELERY_BEAT_SCHEDULER_TIME=1
# Celery schedule tasks configuration
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
ENABLE_CLEAN_MESSAGES=false
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
ENABLE_DATASETS_QUEUE_MONITOR=false
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true
# Position configuration # Position configuration
POSITION_TOOL_PINS= POSITION_TOOL_PINS=
POSITION_TOOL_INCLUDES= POSITION_TOOL_INCLUDES=

@ -74,7 +74,12 @@
10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service. 10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
```bash ```bash
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin
```
Addition, if you want to debug the celery scheduled tasks, you can use the following command in another terminal:
```bash
uv run celery -A app.celery beat
``` ```
## Testing ## Testing

@ -779,6 +779,41 @@ class CeleryBeatConfig(BaseSettings):
) )
class CeleryScheduleTasksConfig(BaseSettings):
ENABLE_CLEAN_EMBEDDING_CACHE_TASK: bool = Field(
description="Enable clean embedding cache task",
default=False,
)
ENABLE_CLEAN_UNUSED_DATASETS_TASK: bool = Field(
description="Enable clean unused datasets task",
default=False,
)
ENABLE_CREATE_TIDB_SERVERLESS_TASK: bool = Field(
description="Enable create tidb service job task",
default=False,
)
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: bool = Field(
description="Enable update tidb service job status task",
default=False,
)
ENABLE_CLEAN_MESSAGES: bool = Field(
description="Enable clean messages task",
default=False,
)
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: bool = Field(
description="Enable mail clean document notify task",
default=False,
)
ENABLE_DATASETS_QUEUE_MONITOR: bool = Field(
description="Enable queue monitor task",
default=False,
)
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: bool = Field(
description="Enable check upgradable plugin task",
default=True,
)
class PositionConfig(BaseSettings): class PositionConfig(BaseSettings):
POSITION_PROVIDER_PINS: str = Field( POSITION_PROVIDER_PINS: str = Field(
description="Comma-separated list of pinned model providers", description="Comma-separated list of pinned model providers",
@ -907,5 +942,6 @@ class FeatureConfig(
# hosted services config # hosted services config
HostedServiceConfig, HostedServiceConfig,
CeleryBeatConfig, CeleryBeatConfig,
CeleryScheduleTasksConfig,
): ):
pass pass

@ -12,7 +12,8 @@ from controllers.console.wraps import account_initialization_required, setup_req
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.exc import PluginDaemonClientSideError
from libs.login import login_required from libs.login import login_required
from models.account import TenantPluginPermission from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_parameter_service import PluginParameterService
from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_permission_service import PluginPermissionService
from services.plugin.plugin_service import PluginService from services.plugin.plugin_service import PluginService
@ -534,6 +535,114 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
return jsonable_encoder({"options": options}) return jsonable_encoder({"options": options})
class PluginChangePreferencesApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
user = current_user
if not user.is_admin_or_owner:
raise Forbidden()
req = reqparse.RequestParser()
req.add_argument("permission", type=dict, required=True, location="json")
req.add_argument("auto_upgrade", type=dict, required=True, location="json")
args = req.parse_args()
tenant_id = user.current_tenant_id
permission = args["permission"]
install_permission = TenantPluginPermission.InstallPermission(permission.get("install_permission", "everyone"))
debug_permission = TenantPluginPermission.DebugPermission(permission.get("debug_permission", "everyone"))
auto_upgrade = args["auto_upgrade"]
strategy_setting = TenantPluginAutoUpgradeStrategy.StrategySetting(
auto_upgrade.get("strategy_setting", "fix_only")
)
upgrade_time_of_day = auto_upgrade.get("upgrade_time_of_day", 0)
upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode(auto_upgrade.get("upgrade_mode", "exclude"))
exclude_plugins = auto_upgrade.get("exclude_plugins", [])
include_plugins = auto_upgrade.get("include_plugins", [])
# set permission
set_permission_result = PluginPermissionService.change_permission(
tenant_id,
install_permission,
debug_permission,
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
# set auto upgrade strategy
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
tenant_id,
strategy_setting,
upgrade_time_of_day,
upgrade_mode,
exclude_plugins,
include_plugins,
)
if not set_auto_upgrade_strategy_result:
return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"})
return jsonable_encoder({"success": True})
class PluginFetchPreferencesApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
tenant_id = current_user.current_tenant_id
permission = PluginPermissionService.get_permission(tenant_id)
permission_dict = {
"install_permission": TenantPluginPermission.InstallPermission.EVERYONE,
"debug_permission": TenantPluginPermission.DebugPermission.EVERYONE,
}
if permission:
permission_dict["install_permission"] = permission.install_permission
permission_dict["debug_permission"] = permission.debug_permission
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id)
auto_upgrade_dict = {
"strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
"upgrade_time_of_day": 0,
"upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
"exclude_plugins": [],
"include_plugins": [],
}
if auto_upgrade:
auto_upgrade_dict = {
"strategy_setting": auto_upgrade.strategy_setting,
"upgrade_time_of_day": auto_upgrade.upgrade_time_of_day,
"upgrade_mode": auto_upgrade.upgrade_mode,
"exclude_plugins": auto_upgrade.exclude_plugins,
"include_plugins": auto_upgrade.include_plugins,
}
return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict})
class PluginAutoUpgradeExcludePluginApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
# exclude one single plugin
tenant_id = current_user.current_tenant_id
req = reqparse.RequestParser()
req.add_argument("plugin_id", type=str, required=True, location="json")
args = req.parse_args()
return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args["plugin_id"])})
api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key")
api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListApi, "/workspaces/current/plugin/list")
api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions")
@ -560,3 +669,7 @@ api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permissi
api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch")
api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options")
api.add_resource(PluginFetchPreferencesApi, "/workspaces/current/plugin/preferences/fetch")
api.add_resource(PluginChangePreferencesApi, "/workspaces/current/plugin/preferences/change")
api.add_resource(PluginAutoUpgradeExcludePluginApi, "/workspaces/current/plugin/preferences/autoupgrade/exclude")

@ -25,9 +25,29 @@ def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplaceP
url = str(marketplace_api_url / "api/v1/plugins/batch") url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids}) response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status() response.raise_for_status()
return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]] return [MarketplacePluginDeclaration(**plugin) for plugin in response.json()["data"]["plugins"]]
def batch_fetch_plugin_manifests_ignore_deserialization_error(
plugin_ids: list[str],
) -> Sequence[MarketplacePluginDeclaration]:
if len(plugin_ids) == 0:
return []
url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status()
result: list[MarketplacePluginDeclaration] = []
for plugin in response.json()["data"]["plugins"]:
try:
result.append(MarketplacePluginDeclaration(**plugin))
except Exception as e:
pass
return result
def record_install_plugin_event(plugin_unique_identifier: str): def record_install_plugin_event(plugin_unique_identifier: str):
url = str(marketplace_api_url / "api/v1/stats/plugins/install_count") url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
response = requests.post(url, json={"unique_identifier": plugin_unique_identifier}) response = requests.post(url, json={"unique_identifier": plugin_unique_identifier})

@ -232,14 +232,14 @@ class WorkflowLoggingCallback(WorkflowCallback):
Publish loop started Publish loop started
""" """
self.print_text("\n[LoopRunStartedEvent]", color="blue") self.print_text("\n[LoopRunStartedEvent]", color="blue")
self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue")
def on_workflow_loop_next(self, event: LoopRunNextEvent) -> None: def on_workflow_loop_next(self, event: LoopRunNextEvent) -> None:
""" """
Publish loop next Publish loop next
""" """
self.print_text("\n[LoopRunNextEvent]", color="blue") self.print_text("\n[LoopRunNextEvent]", color="blue")
self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue")
self.print_text(f"Loop Index: {event.index}", color="blue") self.print_text(f"Loop Index: {event.index}", color="blue")
def on_workflow_loop_completed(self, event: LoopRunSucceededEvent | LoopRunFailedEvent) -> None: def on_workflow_loop_completed(self, event: LoopRunSucceededEvent | LoopRunFailedEvent) -> None:
@ -250,7 +250,7 @@ class WorkflowLoggingCallback(WorkflowCallback):
"\n[LoopRunSucceededEvent]" if isinstance(event, LoopRunSucceededEvent) else "\n[LoopRunFailedEvent]", "\n[LoopRunSucceededEvent]" if isinstance(event, LoopRunSucceededEvent) else "\n[LoopRunFailedEvent]",
color="blue", color="blue",
) )
self.print_text(f"Node ID: {event.loop_id}", color="blue") self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue")
def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None: def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None:
"""Print text with highlighting and no end characters.""" """Print text with highlighting and no end characters."""

@ -334,7 +334,7 @@ class Graph(BaseModel):
parallel = GraphParallel( parallel = GraphParallel(
start_from_node_id=start_node_id, start_from_node_id=start_node_id,
parent_parallel_id=parent_parallel.id if parent_parallel else None, parent_parallel_id=parent_parallel_id,
parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None, parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None,
) )
parallel_mapping[parallel.id] = parallel parallel_mapping[parallel.id] = parallel

@ -22,7 +22,7 @@ if [[ "${MODE}" == "worker" ]]; then
exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \ exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \
--max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \ --max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion} -Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion,plugin}
elif [[ "${MODE}" == "beat" ]]; then elif [[ "${MODE}" == "beat" ]]; then
exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO} exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO}

@ -64,49 +64,62 @@ def init_app(app: DifyApp) -> Celery:
celery_app.set_default() celery_app.set_default()
app.extensions["celery"] = celery_app app.extensions["celery"] = celery_app
imports = [ imports = []
"schedule.clean_embedding_cache_task",
"schedule.clean_unused_datasets_task",
"schedule.create_tidb_serverless_task",
"schedule.update_tidb_serverless_status_task",
"schedule.clean_messages",
"schedule.mail_clean_document_notify_task",
"schedule.queue_monitor_task",
]
day = dify_config.CELERY_BEAT_SCHEDULER_TIME day = dify_config.CELERY_BEAT_SCHEDULER_TIME
beat_schedule = {
"clean_embedding_cache_task": { # if you add a new task, please add the switch to CeleryScheduleTasksConfig
beat_schedule = {}
if dify_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK:
imports.append("schedule.clean_embedding_cache_task")
beat_schedule["clean_embedding_cache_task"] = {
"task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task", "task": "schedule.clean_embedding_cache_task.clean_embedding_cache_task",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
}, }
"clean_unused_datasets_task": { if dify_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK:
imports.append("schedule.clean_unused_datasets_task")
beat_schedule["clean_unused_datasets_task"] = {
"task": "schedule.clean_unused_datasets_task.clean_unused_datasets_task", "task": "schedule.clean_unused_datasets_task.clean_unused_datasets_task",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
}, }
"create_tidb_serverless_task": { if dify_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK:
imports.append("schedule.create_tidb_serverless_task")
beat_schedule["create_tidb_serverless_task"] = {
"task": "schedule.create_tidb_serverless_task.create_tidb_serverless_task", "task": "schedule.create_tidb_serverless_task.create_tidb_serverless_task",
"schedule": crontab(minute="0", hour="*"), "schedule": crontab(minute="0", hour="*"),
}, }
"update_tidb_serverless_status_task": { if dify_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:
imports.append("schedule.update_tidb_serverless_status_task")
beat_schedule["update_tidb_serverless_status_task"] = {
"task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task", "task": "schedule.update_tidb_serverless_status_task.update_tidb_serverless_status_task",
"schedule": timedelta(minutes=10), "schedule": timedelta(minutes=10),
}, }
"clean_messages": { if dify_config.ENABLE_CLEAN_MESSAGES:
imports.append("schedule.clean_messages")
beat_schedule["clean_messages"] = {
"task": "schedule.clean_messages.clean_messages", "task": "schedule.clean_messages.clean_messages",
"schedule": timedelta(days=day), "schedule": timedelta(days=day),
}, }
# every Monday if dify_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:
"mail_clean_document_notify_task": { imports.append("schedule.mail_clean_document_notify_task")
beat_schedule["mail_clean_document_notify_task"] = {
"task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task", "task": "schedule.mail_clean_document_notify_task.mail_clean_document_notify_task",
"schedule": crontab(minute="0", hour="10", day_of_week="1"), "schedule": crontab(minute="0", hour="10", day_of_week="1"),
}, }
"datasets-queue-monitor": { if dify_config.ENABLE_DATASETS_QUEUE_MONITOR:
imports.append("schedule.queue_monitor_task")
beat_schedule["datasets-queue-monitor"] = {
"task": "schedule.queue_monitor_task.queue_monitor_task", "task": "schedule.queue_monitor_task.queue_monitor_task",
"schedule": timedelta( "schedule": timedelta(
minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30 minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30
), ),
}, }
} if dify_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:
imports.append("schedule.check_upgradable_plugin_task")
beat_schedule["check_upgradable_plugin_task"] = {
"task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task",
"schedule": crontab(minute="*/15"),
}
celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)
return celery_app return celery_app

@ -0,0 +1,41 @@
"""empty message
Revision ID: 16081485540c
Revises: d28f2004b072
Create Date: 2025-05-15 16:35:39.113777
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '16081485540c'
down_revision = '58eb7bdb93fe'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tenant_plugin_auto_upgrade_strategies',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('strategy_setting', sa.String(length=16), server_default='fix_only', nullable=False),
sa.Column('upgrade_time_of_day', sa.Integer(), nullable=False),
sa.Column('upgrade_mode', sa.String(length=16), server_default='exclude', nullable=False),
sa.Column('exclude_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
sa.Column('include_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='tenant_plugin_auto_upgrade_strategy_pkey'),
sa.UniqueConstraint('tenant_id', name='unique_tenant_plugin_auto_upgrade_strategy')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('tenant_plugin_auto_upgrade_strategies')
# ### end Alembic commands ###

@ -299,3 +299,35 @@ class TenantPluginPermission(Base):
db.String(16), nullable=False, server_default="everyone" db.String(16), nullable=False, server_default="everyone"
) )
debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone") debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), nullable=False, server_default="noone")
class TenantPluginAutoUpgradeStrategy(Base):
class StrategySetting(enum.StrEnum):
DISABLED = "disabled"
FIX_ONLY = "fix_only"
LATEST = "latest"
class UpgradeMode(enum.StrEnum):
ALL = "all"
PARTIAL = "partial"
EXCLUDE = "exclude"
__tablename__ = "tenant_plugin_auto_upgrade_strategies"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"),
db.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"),
)
id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
strategy_setting: Mapped[StrategySetting] = mapped_column(db.String(16), nullable=False, server_default="fix_only")
upgrade_time_of_day: Mapped[int] = mapped_column(db.Integer, nullable=False, default=0) # seconds of the day
upgrade_mode: Mapped[UpgradeMode] = mapped_column(db.String(16), nullable=False, server_default="exclude")
exclude_plugins: Mapped[list[str]] = mapped_column(
db.ARRAY(db.String(255)), nullable=False
) # plugin_id (author/name)
include_plugins: Mapped[list[str]] = mapped_column(
db.ARRAY(db.String(255)), nullable=False
) # plugin_id (author/name)
created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())

@ -0,0 +1,49 @@
import time
import click
import app
from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy
from tasks.process_tenant_plugin_autoupgrade_check_task import process_tenant_plugin_autoupgrade_check_task
AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes
@app.celery.task(queue="plugin")
def check_upgradable_plugin_task():
click.echo(click.style("Start check upgradable plugin.", fg="green"))
start_at = time.perf_counter()
now_seconds_of_day = time.time() % 86400 - 30 # we assume the tz is UTC
click.echo(click.style("Now seconds of day: {}".format(now_seconds_of_day), fg="green"))
strategies = (
db.session.query(TenantPluginAutoUpgradeStrategy)
.filter(
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
TenantPluginAutoUpgradeStrategy.strategy_setting
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
)
.all()
)
for strategy in strategies:
process_tenant_plugin_autoupgrade_check_task.delay(
strategy.tenant_id,
strategy.strategy_setting,
strategy.upgrade_time_of_day,
strategy.upgrade_mode,
strategy.exclude_plugins,
strategy.include_plugins,
)
end_at = time.perf_counter()
click.echo(
click.style(
"Checked upgradable plugin success latency: {}".format(end_at - start_at),
fg="green",
)
)

@ -28,6 +28,7 @@ from models.account import (
Tenant, Tenant,
TenantAccountJoin, TenantAccountJoin,
TenantAccountRole, TenantAccountRole,
TenantPluginAutoUpgradeStrategy,
TenantStatus, TenantStatus,
) )
from models.model import DifySetup from models.model import DifySetup
@ -611,6 +612,17 @@ class TenantService:
db.session.add(tenant) db.session.add(tenant)
db.session.commit() db.session.commit()
plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy(
tenant_id=tenant.id,
strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
upgrade_time_of_day=0,
upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
exclude_plugins=[],
include_plugins=[],
)
db.session.add(plugin_upgrade_strategy)
db.session.commit()
tenant.encrypt_public_key = generate_key_pair(tenant.id) tenant.encrypt_public_key = generate_key_pair(tenant.id)
db.session.commit() db.session.commit()
return tenant return tenant

@ -0,0 +1,87 @@
from sqlalchemy.orm import Session
from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy
class PluginAutoUpgradeService:
@staticmethod
def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None:
with Session(db.engine) as session:
return (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)
@staticmethod
def change_strategy(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
) -> bool:
with Session(db.engine) as session:
exist_strategy = (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)
if not exist_strategy:
strategy = TenantPluginAutoUpgradeStrategy(
tenant_id=tenant_id,
strategy_setting=strategy_setting,
upgrade_time_of_day=upgrade_time_of_day,
upgrade_mode=upgrade_mode,
exclude_plugins=exclude_plugins,
include_plugins=include_plugins,
)
session.add(strategy)
else:
exist_strategy.strategy_setting = strategy_setting
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
exist_strategy.upgrade_mode = upgrade_mode
exist_strategy.exclude_plugins = exclude_plugins
exist_strategy.include_plugins = include_plugins
session.commit()
return True
@staticmethod
def exclude_plugin(tenant_id: str, plugin_id: str) -> bool:
with Session(db.engine) as session:
exist_strategy = (
session.query(TenantPluginAutoUpgradeStrategy)
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
.first()
)
if not exist_strategy:
# create for this tenant
PluginAutoUpgradeService.change_strategy(
tenant_id,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
0,
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
[plugin_id],
[],
)
return True
else:
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
if plugin_id not in exist_strategy.exclude_plugins:
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
new_exclude_plugins.append(plugin_id)
exist_strategy.exclude_plugins = new_exclude_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
if plugin_id in exist_strategy.include_plugins:
new_include_plugins = exist_strategy.include_plugins.copy()
new_include_plugins.remove(plugin_id)
exist_strategy.include_plugins = new_include_plugins
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
exist_strategy.exclude_plugins = [plugin_id]
session.commit()
return True

@ -0,0 +1,166 @@
import traceback
import typing
import click
from celery import shared_task # type: ignore
from core.helper import marketplace
from core.helper.marketplace import MarketplacePluginDeclaration
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from models.account import TenantPluginAutoUpgradeStrategy
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
cached_plugin_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {}
def marketplace_batch_fetch_plugin_manifests(
plugin_ids_plain_list: list[str],
) -> list[MarketplacePluginDeclaration]:
global cached_plugin_manifests
# return marketplace.batch_fetch_plugin_manifests(plugin_ids_plain_list)
not_included_plugin_ids = [
plugin_id for plugin_id in plugin_ids_plain_list if plugin_id not in cached_plugin_manifests
]
if not_included_plugin_ids:
manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_included_plugin_ids)
for manifest in manifests:
cached_plugin_manifests[manifest.plugin_id] = manifest
if (
len(manifests) == 0
): # this indicates that the plugin not found in marketplace, should set None in cache to prevent future check
for plugin_id in not_included_plugin_ids:
cached_plugin_manifests[plugin_id] = None
result: list[MarketplacePluginDeclaration] = []
for plugin_id in plugin_ids_plain_list:
final_manifest = cached_plugin_manifests.get(plugin_id)
if final_manifest is not None:
result.append(final_manifest)
return result
@shared_task(queue="plugin")
def process_tenant_plugin_autoupgrade_check_task(
tenant_id: str,
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
upgrade_time_of_day: int,
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
exclude_plugins: list[str],
include_plugins: list[str],
):
try:
manager = PluginInstaller()
click.echo(
click.style(
"Checking upgradable plugin for tenant: {}".format(tenant_id),
fg="green",
)
)
if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
return
# get plugin_ids to check
plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
click.echo(click.style("Upgrade mode: {}".format(upgrade_mode), fg="green"))
if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
all_plugins = manager.list_plugins(tenant_id)
for plugin in all_plugins:
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
plugin_ids.append(
(
plugin.plugin_id,
plugin.version,
plugin.plugin_unique_identifier,
)
)
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
# get all plugins and remove excluded plugins
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
]
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
all_plugins = manager.list_plugins(tenant_id)
plugin_ids = [
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
for plugin in all_plugins
if plugin.source == PluginInstallationSource.Marketplace
]
if not plugin_ids:
return
plugin_ids_plain_list = [plugin_id for plugin_id, _, _ in plugin_ids]
manifests = marketplace_batch_fetch_plugin_manifests(plugin_ids_plain_list)
if not manifests:
return
for manifest in manifests:
for plugin_id, version, original_unique_identifier in plugin_ids:
if manifest.plugin_id != plugin_id:
continue
try:
current_version = version
latest_version = manifest.latest_version
def fix_only_checker(latest_version, current_version):
latest_version_tuple = tuple(int(val) for val in latest_version.split("."))
current_version_tuple = tuple(int(val) for val in current_version.split("."))
if (
latest_version_tuple[0] == current_version_tuple[0]
and latest_version_tuple[1] == current_version_tuple[1]
):
return latest_version_tuple[2] != current_version_tuple[2]
return False
version_checker = {
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: lambda latest_version,
current_version: latest_version != current_version,
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
}
if version_checker[strategy_setting](latest_version, current_version):
# execute upgrade
new_unique_identifier = manifest.latest_package_identifier
marketplace.record_install_plugin_event(new_unique_identifier)
click.echo(
click.style(
"Upgrade plugin: {} -> {}".format(original_unique_identifier, new_unique_identifier),
fg="green",
)
)
task_start_resp = manager.upgrade_plugin(
tenant_id,
original_unique_identifier,
new_unique_identifier,
PluginInstallationSource.Marketplace,
{
"plugin_unique_identifier": new_unique_identifier,
},
)
except Exception as e:
click.echo(click.style("Error when upgrading plugin: {}".format(e), fg="red"))
traceback.print_exc()
break
except Exception as e:
click.echo(click.style("Error when checking upgradable plugin: {}".format(e), fg="red"))
traceback.print_exc()
return

@ -1138,3 +1138,13 @@ QUEUE_MONITOR_THRESHOLD=200
QUEUE_MONITOR_ALERT_EMAILS= QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes # Monitor interval in minutes, default is 30 minutes
QUEUE_MONITOR_INTERVAL=30 QUEUE_MONITOR_INTERVAL=30
# Celery schedule tasks configuration
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=false
ENABLE_CLEAN_UNUSED_DATASETS_TASK=false
ENABLE_CREATE_TIDB_SERVERLESS_TASK=false
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=false
ENABLE_CLEAN_MESSAGES=false
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=false
ENABLE_DATASETS_QUEUE_MONITOR=false
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=true

@ -55,6 +55,25 @@ services:
- ssrf_proxy_network - ssrf_proxy_network
- default - default
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.5.0
restart: always
environment:
# Use the shared environment variables.
<<: *shared-api-worker-env
# Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks.
MODE: beat
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- ssrf_proxy_network
- default
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.5.1 image: langgenius/dify-web:1.5.1

@ -514,6 +514,14 @@ x-shared-env: &shared-api-worker-env
QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200} QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200}
QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-} QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-}
QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30} QUEUE_MONITOR_INTERVAL: ${QUEUE_MONITOR_INTERVAL:-30}
ENABLE_CLEAN_EMBEDDING_CACHE_TASK: ${ENABLE_CLEAN_EMBEDDING_CACHE_TASK:-false}
ENABLE_CLEAN_UNUSED_DATASETS_TASK: ${ENABLE_CLEAN_UNUSED_DATASETS_TASK:-false}
ENABLE_CREATE_TIDB_SERVERLESS_TASK: ${ENABLE_CREATE_TIDB_SERVERLESS_TASK:-false}
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK: ${ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK:-false}
ENABLE_CLEAN_MESSAGES: ${ENABLE_CLEAN_MESSAGES:-false}
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK: ${ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK:-false}
ENABLE_DATASETS_QUEUE_MONITOR: ${ENABLE_DATASETS_QUEUE_MONITOR:-false}
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK: ${ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:-true}
services: services:
# API service # API service
@ -571,6 +579,25 @@ services:
- ssrf_proxy_network - ssrf_proxy_network
- default - default
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.5.0
restart: always
environment:
# Use the shared environment variables.
<<: *shared-api-worker-env
# Startup mode, 'worker_beat' starts the Celery beat for scheduling periodic tasks.
MODE: beat
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- ssrf_proxy_network
- default
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:1.5.1 image: langgenius/dify-web:1.5.1

@ -4,18 +4,21 @@ import cn from '@/utils/classnames'
type OptionListItemProps = { type OptionListItemProps = {
isSelected: boolean isSelected: boolean
onClick: () => void onClick: () => void
noAutoScroll?: boolean
} & React.LiHTMLAttributes<HTMLLIElement> } & React.LiHTMLAttributes<HTMLLIElement>
const OptionListItem: FC<OptionListItemProps> = ({ const OptionListItem: FC<OptionListItemProps> = ({
isSelected, isSelected,
onClick, onClick,
noAutoScroll,
children, children,
}) => { }) => {
const listItemRef = useRef<HTMLLIElement>(null) const listItemRef = useRef<HTMLLIElement>(null)
useEffect(() => { useEffect(() => {
if (isSelected) if (isSelected && !noAutoScroll)
listItemRef.current?.scrollIntoView({ behavior: 'instant' }) listItemRef.current?.scrollIntoView({ behavior: 'instant' })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return ( return (

@ -1,13 +1,18 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const Header = () => { type Props = {
title?: string
}
const Header = ({
title,
}: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex flex-col border-b-[0.5px] border-divider-regular'> <div className='flex flex-col border-b-[0.5px] border-divider-regular'>
<div className='system-md-semibold flex items-center px-2 py-1.5 text-text-primary'> <div className='system-md-semibold flex items-center px-2 py-1.5 text-text-primary'>
{t('time.title.pickTime')} {title || t('time.title.pickTime')}
</div> </div>
</div> </div>
) )

@ -20,6 +20,9 @@ const TimePicker = ({
onChange, onChange,
onClear, onClear,
renderTrigger, renderTrigger,
title,
minuteFilter,
popupClassName,
}: TimePickerProps) => { }: TimePickerProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@ -108,6 +111,15 @@ const TimePicker = ({
const displayValue = value?.format(timeFormat) || '' const displayValue = value?.format(timeFormat) || ''
const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder')) const placeholderDate = isOpen && selectedTime ? selectedTime.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
const inputElem = (
<input
className='system-xs-regular flex-1 cursor-pointer appearance-none truncate bg-transparent p-1
text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder'
readOnly
value={isOpen ? '' : displayValue}
placeholder={placeholderDate}
/>
)
return ( return (
<PortalToFollowElem <PortalToFollowElem
open={isOpen} open={isOpen}
@ -115,18 +127,16 @@ const TimePicker = ({
placement='bottom-end' placement='bottom-end'
> >
<PortalToFollowElemTrigger> <PortalToFollowElemTrigger>
{renderTrigger ? (renderTrigger()) : ( {renderTrigger ? (renderTrigger({
inputElem,
onClick: handleClickTrigger,
isOpen,
})) : (
<div <div
className='group flex w-[252px] cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt' className='group flex w-[252px] cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt'
onClick={handleClickTrigger} onClick={handleClickTrigger}
> >
<input {inputElem}
className='system-xs-regular flex-1 cursor-pointer appearance-none truncate bg-transparent p-1
text-components-input-text-filled outline-none placeholder:text-components-input-text-placeholder'
readOnly
value={isOpen ? '' : displayValue}
placeholder={placeholderDate}
/>
<RiTimeLine className={cn( <RiTimeLine className={cn(
'h-4 w-4 shrink-0 text-text-quaternary', 'h-4 w-4 shrink-0 text-text-quaternary',
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
@ -142,14 +152,15 @@ const TimePicker = ({
</div> </div>
)} )}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'> <PortalToFollowElemContent className={cn('z-50', popupClassName)}>
<div className='mt-1 w-[252px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'> <div className='mt-1 w-[252px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5'>
{/* Header */} {/* Header */}
<Header /> <Header title={title} />
{/* Time Options */} {/* Time Options */}
<Options <Options
selectedTime={selectedTime} selectedTime={selectedTime}
minuteFilter={minuteFilter}
handleSelectHour={handleSelectHour} handleSelectHour={handleSelectHour}
handleSelectMinute={handleSelectMinute} handleSelectMinute={handleSelectMinute}
handleSelectPeriod={handleSelectPeriod} handleSelectPeriod={handleSelectPeriod}

@ -5,6 +5,7 @@ import OptionListItem from '../common/option-list-item'
const Options: FC<TimeOptionsProps> = ({ const Options: FC<TimeOptionsProps> = ({
selectedTime, selectedTime,
minuteFilter,
handleSelectHour, handleSelectHour,
handleSelectMinute, handleSelectMinute,
handleSelectPeriod, handleSelectPeriod,
@ -33,7 +34,7 @@ const Options: FC<TimeOptionsProps> = ({
{/* Minute */} {/* Minute */}
<ul className='no-scrollbar flex h-[208px] flex-col gap-y-0.5 overflow-y-auto pb-[184px]'> <ul className='no-scrollbar flex h-[208px] flex-col gap-y-0.5 overflow-y-auto pb-[184px]'>
{ {
minuteOptions.map((minute) => { (minuteFilter ? minuteFilter(minuteOptions) : minuteOptions).map((minute) => {
const isSelected = selectedTime?.format('mm') === minute const isSelected = selectedTime?.format('mm') === minute
return ( return (
<OptionListItem <OptionListItem
@ -57,6 +58,7 @@ const Options: FC<TimeOptionsProps> = ({
key={period} key={period}
isSelected={isSelected} isSelected={isSelected}
onClick={handleSelectPeriod.bind(null, period)} onClick={handleSelectPeriod.bind(null, period)}
noAutoScroll // if choose PM which would hide(scrolled) AM that may make user confused that there's no am.
> >
{period} {period}
</OptionListItem> </OptionListItem>

@ -28,6 +28,7 @@ export type DatePickerProps = {
onClear: () => void onClear: () => void
triggerWrapClassName?: string triggerWrapClassName?: string
renderTrigger?: (props: TriggerProps) => React.ReactNode renderTrigger?: (props: TriggerProps) => React.ReactNode
minuteFilter?: (minutes: string[]) => string[]
popupZIndexClassname?: string popupZIndexClassname?: string
} }
@ -47,13 +48,21 @@ export type DatePickerFooterProps = {
handleConfirmDate: () => void handleConfirmDate: () => void
} }
export type TriggerParams = {
isOpen: boolean
inputElem: React.ReactNode
onClick: (e: React.MouseEvent) => void
}
export type TimePickerProps = { export type TimePickerProps = {
value: Dayjs | undefined value: Dayjs | undefined
timezone?: string timezone?: string
placeholder?: string placeholder?: string
onChange: (date: Dayjs | undefined) => void onChange: (date: Dayjs | undefined) => void
onClear: () => void onClear: () => void
renderTrigger?: () => React.ReactNode renderTrigger?: (props: TriggerParams) => React.ReactNode
title?: string
minuteFilter?: (minutes: string[]) => string[]
popupClassName?: string
} }
export type TimePickerFooterProps = { export type TimePickerFooterProps = {
@ -81,6 +90,7 @@ export type CalendarItemProps = {
export type TimeOptionsProps = { export type TimeOptionsProps = {
selectedTime: Dayjs | undefined selectedTime: Dayjs | undefined
minuteFilter?: (minutes: string[]) => string[]
handleSelectHour: (hour: string) => void handleSelectHour: (hour: string) => void
handleSelectMinute: (minute: string) => void handleSelectMinute: (minute: string) => void
handleSelectPeriod: (period: Period) => void handleSelectPeriod: (period: Period) => void

@ -2,6 +2,7 @@ import dayjs, { type Dayjs } from 'dayjs'
import type { Day } from '../types' import type { Day } from '../types'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
import tz from '@/utils/timezone.json'
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -78,3 +79,14 @@ export const getHourIn12Hour = (date: Dayjs) => {
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => { export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone) return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone)
} }
// Asia/Shanghai -> UTC+8
const DEFAULT_OFFSET_STR = 'UTC+0'
export const convertTimezoneToOffsetStr = (timezone?: string) => {
if (!timezone)
return DEFAULT_OFFSET_STR
const tzItem = tz.find(item => item.value === timezone)
if(!tzItem)
return DEFAULT_OFFSET_STR
return `UTC${tzItem.name.charAt(0)}${tzItem.name.charAt(2)}`
}

@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3308 16H14.2915L13.6249 13.9476H10.3761L9.70846 16H7.66918L10.7759 7H13.2281L16.3308 16ZM10.8595 12.4622H13.1435L12.0378 9.05639H11.9673L10.8595 12.4622Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

@ -75,7 +75,7 @@ Icon.displayName = '<%= svgName %>'
export default Icon export default Icon
`.trim()) `.trim())
await writeFile(path.resolve(currentPath, `${fileName}.json`), JSON.stringify(svgData, '', '\t')) await writeFile(path.resolve(currentPath, `${fileName}.json`), `${JSON.stringify(svgData, '', '\t')}\n`)
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`) await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`)
const indexingRender = template(` const indexingRender = template(`

File diff suppressed because one or more lines are too long

@ -4,12 +4,16 @@
import * as React from 'react' import * as React from 'react'
import data from './AliyunIcon.json' import data from './AliyunIcon.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( const Icon = (
props, {
ref, ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />) ...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AliyunIcon' Icon.displayName = 'AliyunIcon'

File diff suppressed because one or more lines are too long

@ -4,12 +4,16 @@
import * as React from 'react' import * as React from 'react'
import data from './AliyunIconBig.json' import data from './AliyunIconBig.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( const Icon = (
props, {
ref, ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />) ...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AliyunIconBig' Icon.displayName = 'AliyunIconBig'

@ -4,12 +4,16 @@
import * as React from 'react' import * as React from 'react'
import data from './WeaveIcon.json' import data from './WeaveIcon.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( const Icon = (
props, {
ref, ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />) ...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'WeaveIcon' Icon.displayName = 'WeaveIcon'

@ -4,12 +4,16 @@
import * as React from 'react' import * as React from 'react'
import data from './WeaveIconBig.json' import data from './WeaveIconBig.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( const Icon = (
props, {
ref, ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />) ...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'WeaveIconBig' Icon.displayName = 'WeaveIconBig'

@ -1,3 +1,5 @@
export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'
export { default as ArizeIconBig } from './ArizeIconBig' export { default as ArizeIconBig } from './ArizeIconBig'
export { default as ArizeIcon } from './ArizeIcon' export { default as ArizeIcon } from './ArizeIcon'
export { default as LangfuseIconBig } from './LangfuseIconBig' export { default as LangfuseIconBig } from './LangfuseIconBig'
@ -11,5 +13,3 @@ export { default as PhoenixIcon } from './PhoenixIcon'
export { default as TracingIcon } from './TracingIcon' export { default as TracingIcon } from './TracingIcon'
export { default as WeaveIconBig } from './WeaveIconBig' export { default as WeaveIconBig } from './WeaveIconBig'
export { default as WeaveIcon } from './WeaveIcon' export { default as WeaveIcon } from './WeaveIcon'
export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'

@ -23,4 +23,4 @@
] ]
}, },
"name": "Citations" "name": "Citations"
} }

@ -25,4 +25,4 @@
] ]
}, },
"name": "ContentModeration" "name": "ContentModeration"
} }

@ -20,4 +20,4 @@
] ]
}, },
"name": "Document" "name": "Document"
} }

@ -23,4 +23,4 @@
] ]
}, },
"name": "FolderUpload" "name": "FolderUpload"
} }

@ -23,4 +23,4 @@
] ]
}, },
"name": "LoveMessage" "name": "LoveMessage"
} }

@ -25,4 +25,4 @@
] ]
}, },
"name": "MessageFast" "name": "MessageFast"
} }

@ -34,4 +34,4 @@
] ]
}, },
"name": "Microphone01" "name": "Microphone01"
} }

@ -74,4 +74,4 @@
] ]
}, },
"name": "TextToAudio" "name": "TextToAudio"
} }

@ -32,4 +32,4 @@
] ]
}, },
"name": "VirtualAssistant" "name": "VirtualAssistant"
} }

@ -25,4 +25,4 @@
] ]
}, },
"name": "Vision" "name": "Vision"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "AlertTriangle" "name": "AlertTriangle"
} }

@ -63,4 +63,4 @@
] ]
}, },
"name": "ThumbsDown" "name": "ThumbsDown"
} }

@ -63,4 +63,4 @@
] ]
}, },
"name": "ThumbsUp" "name": "ThumbsUp"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "ArrowNarrowLeft" "name": "ArrowNarrowLeft"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "ArrowUpRight" "name": "ArrowUpRight"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "ChevronDownDouble" "name": "ChevronDownDouble"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "ChevronRight" "name": "ChevronRight"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "ChevronSelectorVertical" "name": "ChevronSelectorVertical"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "RefreshCcw01" "name": "RefreshCcw01"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "RefreshCw05" "name": "RefreshCw05"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "ReverseLeft" "name": "ReverseLeft"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "AiText" "name": "AiText"
} }

@ -90,4 +90,4 @@
] ]
}, },
"name": "ChatBot" "name": "ChatBot"
} }

@ -65,4 +65,4 @@
] ]
}, },
"name": "ChatBotSlim" "name": "ChatBotSlim"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "CuteRobot" "name": "CuteRobot"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "MessageCheckRemove" "name": "MessageCheckRemove"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "MessageFastPlus" "name": "MessageFastPlus"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "ArtificialBrain" "name": "ArtificialBrain"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "BarChartSquare02" "name": "BarChartSquare02"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "BracketsX" "name": "BracketsX"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "CodeBrowser" "name": "CodeBrowser"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "Container" "name": "Container"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "Database01" "name": "Database01"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "Database03" "name": "Database03"
} }

@ -49,4 +49,4 @@
] ]
}, },
"name": "FileHeart02" "name": "FileHeart02"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "GitBranch01" "name": "GitBranch01"
} }

@ -62,4 +62,4 @@
] ]
}, },
"name": "PromptEngineering" "name": "PromptEngineering"
} }

@ -63,4 +63,4 @@
] ]
}, },
"name": "PuzzlePiece01" "name": "PuzzlePiece01"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "TerminalSquare" "name": "TerminalSquare"
} }

@ -59,4 +59,4 @@
] ]
}, },
"name": "Variable" "name": "Variable"
} }

@ -86,4 +86,4 @@
] ]
}, },
"name": "Webhooks" "name": "Webhooks"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "AlignLeft" "name": "AlignLeft"
} }

@ -35,4 +35,4 @@
] ]
}, },
"name": "BezierCurve03" "name": "BezierCurve03"
} }

@ -59,4 +59,4 @@
] ]
}, },
"name": "Collapse" "name": "Collapse"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "Colors" "name": "Colors"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "ImageIndentLeft" "name": "ImageIndentLeft"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "LeftIndent02" "name": "LeftIndent02"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "LetterSpacing01" "name": "LetterSpacing01"
} }

@ -35,4 +35,4 @@
] ]
}, },
"name": "TypeSquare" "name": "TypeSquare"
} }

@ -46,4 +46,4 @@
] ]
}, },
"name": "BookOpen01" "name": "BookOpen01"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "Clipboard" "name": "Clipboard"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "ClipboardCheck" "name": "ClipboardCheck"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "File02" "name": "File02"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "FileArrow01" "name": "FileArrow01"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "FileCheck02" "name": "FileCheck02"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "FileDownload02" "name": "FileDownload02"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "FilePlus01" "name": "FilePlus01"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "FilePlus02" "name": "FilePlus02"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "FileText" "name": "FileText"
} }

@ -49,4 +49,4 @@
] ]
}, },
"name": "FileUpload" "name": "FileUpload"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "Folder" "name": "Folder"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "Balance" "name": "Balance"
} }

@ -36,4 +36,4 @@
] ]
}, },
"name": "CoinsStacked01" "name": "CoinsStacked01"
} }

@ -117,4 +117,4 @@
] ]
}, },
"name": "GoldCoin" "name": "GoldCoin"
} }

@ -26,4 +26,4 @@
] ]
}, },
"name": "ReceiptList" "name": "ReceiptList"
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save