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'

@ -0,0 +1,77 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "32",
"height": "32",
"viewBox": "0 0 32 32",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"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": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 16H6.67155",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 9.33334H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 22.6667H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M26 22L29.3333 25.3333",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "SearchMenu"
}

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SearchMenu.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'SearchMenu'
export default Icon

@ -19,6 +19,7 @@ export { default as Pin01 } from './Pin01'
export { default as Pin02 } from './Pin02' export { default as Pin02 } from './Pin02'
export { default as Plus02 } from './Plus02' export { default as Plus02 } from './Plus02'
export { default as Refresh } from './Refresh' export { default as Refresh } from './Refresh'
export { default as SearchMenu } from './SearchMenu'
export { default as Settings01 } from './Settings01' export { default as Settings01 } from './Settings01'
export { default as Settings04 } from './Settings04' export { default as Settings04 } from './Settings04'
export { default as Target04 } from './Target04' export { default as Target04 } from './Target04'

@ -0,0 +1,37 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"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": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"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": "currentColor"
},
"children": []
}
]
},
"name": "AutoUpdateLine"
}

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AutoUpdateLine.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AutoUpdateLine'
export default Icon

@ -0,0 +1 @@
export { default as AutoUpdateLine } from './AutoUpdateLine'

@ -10,7 +10,7 @@ import type { ExposeRefs } from './install-multi'
import InstallMulti from './install-multi' import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins' import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useMittContextSelector } from '@/context/mitt-context' import { useMittContextSelector } from '@/context/mitt-context'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'

@ -35,7 +35,12 @@ import { useProviderContext } from '@/context/provider-context'
import { useInvalidateAllToolProviders } from '@/service/use-tools' import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { API_PREFIX } from '@/config' import { API_PREFIX } from '@/config'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { AutoUpdateLine } from '../../base/icons/src/vender/system'
import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils'
import { getMarketplaceUrl } from '@/utils/var' import { getMarketplaceUrl } from '@/utils/var'
import useReferenceSetting from '../plugin-page/use-reference-setting'
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
import { useAppContext } from '@/context/app-context'
const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'
@ -51,6 +56,8 @@ const DetailHeader = ({
onUpdate, onUpdate,
}: Props) => { }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { theme } = useTheme() const { theme } = useTheme()
const locale = useGetLanguage() const locale = useGetLanguage()
const { checkForUpdates, fetchReleases } = useGitHubReleases() const { checkForUpdates, fetchReleases } = useGitHubReleases()
@ -97,8 +104,24 @@ const DetailHeader = ({
setFalse: hideUpdateModal, setFalse: hideUpdateModal,
}] = useBoolean(false) }] = useBoolean(false)
const handleUpdate = async () => { const { referenceSetting } = useReferenceSetting()
const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {}
const isAutoUpgradeEnabled = useMemo(() => {
if (!autoUpgradeInfo || !isFromMarketplace)
return false
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all)
return true
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id))
return true
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id))
return true
return false
}, [autoUpgradeInfo, plugin_id, isFromMarketplace])
const [isDowngrade, setIsDowngrade] = useState(false)
const handleUpdate = async (isDowngrade?: boolean) => {
if (isFromMarketplace) { if (isFromMarketplace) {
setIsDowngrade(!!isDowngrade)
showUpdateModal() showUpdateModal()
return return
} }
@ -165,9 +188,6 @@ const DetailHeader = ({
} }
}, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders]) }, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
// #plugin TODO# used in apps
// const usedInApps = 3
return ( return (
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}> <div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
<div className="flex"> <div className="flex">
@ -186,7 +206,7 @@ const DetailHeader = ({
currentVersion={version} currentVersion={version}
onSelect={(state) => { onSelect={(state) => {
setTargetVersion(state) setTargetVersion(state)
handleUpdate() handleUpdate(state.isDowngrade)
}} }}
trigger={ trigger={
<Badge <Badge
@ -206,6 +226,18 @@ const DetailHeader = ({
/> />
} }
/> />
{/* Auto update info */}
{isAutoUpgradeEnabled && (
<Tooltip popupContent={t('plugin.autoUpdate.nextUpdateTime', { time: timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(autoUpgradeInfo?.upgrade_time_of_day || 0, timezone!)).format('hh:mm A') })}>
{/* add a a div to fix tooltip hover not show problem */}
<div>
<Badge className='mr-1 cursor-pointer px-1'>
<AutoUpdateLine className='size-3' />
</Badge>
</div>
</Tooltip>
)}
{(hasNewVersion || isFromGitHub) && ( {(hasNewVersion || isFromGitHub) && (
<Button variant='secondary-accent' size='small' className='!h-5' onClick={() => { <Button variant='secondary-accent' size='small' className='!h-5' onClick={() => {
if (isFromMarketplace) { if (isFromMarketplace) {
@ -290,6 +322,7 @@ const DetailHeader = ({
{ {
isShowUpdateModal && ( isShowUpdateModal && (
<UpdateFromMarketplace <UpdateFromMarketplace
pluginId={plugin_id}
payload={{ payload={{
category: detail.declaration.category, category: detail.declaration.category,
originalPackageInfo: { originalPackageInfo: {
@ -303,6 +336,7 @@ const DetailHeader = ({
}} }}
onCancel={hideUpdateModal} onCancel={hideUpdateModal}
onSave={handleUpdatedFromMarketplace} onSave={handleUpdatedFromMarketplace}
isShowDowngradeWarningModal={isDowngrade && isAutoUpgradeEnabled}
/> />
) )
} }

@ -17,14 +17,14 @@ import {
} from './context' } from './context'
import InstallPluginDropdown from './install-plugin-dropdown' import InstallPluginDropdown from './install-plugin-dropdown'
import { useUploader } from './use-uploader' import { useUploader } from './use-uploader'
import usePermission from './use-permission' import useReferenceSetting from './use-reference-setting'
import DebugInfo from './debug-info' import DebugInfo from './debug-info'
import PluginTasks from './plugin-tasks' import PluginTasks from './plugin-tasks'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import TabSlider from '@/app/components/base/tab-slider' import TabSlider from '@/app/components/base/tab-slider'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal' import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal/modal'
import InstallFromMarketplace from '../install-plugin/install-from-marketplace' import InstallFromMarketplace from '../install-plugin/install-from-marketplace'
import { import {
useRouter, useRouter,
@ -121,16 +121,16 @@ const PluginPage = ({
}, [packageId, bundleInfo]) }, [packageId, bundleInfo])
const { const {
referenceSetting,
canManagement, canManagement,
canDebugger, canDebugger,
canSetPermissions, canSetPermissions,
permissions, setReferenceSettings,
setPermissions, } = useReferenceSetting()
} = usePermission()
const [showPluginSettingModal, { const [showPluginSettingModal, {
setTrue: setShowPluginSettingModal, setTrue: setShowPluginSettingModal,
setFalse: setHidePluginSettingModal, setFalse: setHidePluginSettingModal,
}] = useBoolean() }] = useBoolean(false)
const [currentFile, setCurrentFile] = useState<File | null>(null) const [currentFile, setCurrentFile] = useState<File | null>(null)
const containerRef = usePluginPageContext(v => v.containerRef) const containerRef = usePluginPageContext(v => v.containerRef)
const options = usePluginPageContext(v => v.options) const options = usePluginPageContext(v => v.options)
@ -276,10 +276,10 @@ const PluginPage = ({
} }
{showPluginSettingModal && ( {showPluginSettingModal && (
<PermissionSetModal <ReferenceSettingModal
payload={permissions!} payload={referenceSetting!}
onHide={setHidePluginSettingModal} onHide={setHidePluginSettingModal}
onSave={setPermissions} onSave={setReferenceSettings}
/> />
)} )}

@ -2,7 +2,7 @@ import { PermissionType } from '../types'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins' import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
@ -19,14 +19,16 @@ const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean)
return isAdmin return isAdmin
} }
const usePermission = () => { const useReferenceSetting = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext() const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
const { data: permissions } = usePermissions() const { data } = useReferenceSettings()
const invalidatePermissions = useInvalidatePermissions() // console.log(data)
const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({ const { permission: permissions } = data || {}
const invalidateReferenceSettings = useInvalidateReferenceSettings()
const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({
onSuccess: () => { onSuccess: () => {
invalidatePermissions() invalidateReferenceSettings()
Toast.notify({ Toast.notify({
type: 'success', type: 'success',
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
@ -36,18 +38,18 @@ const usePermission = () => {
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
return { return {
referenceSetting: data,
setReferenceSettings: updateReferenceSetting,
canManagement: hasPermission(permissions?.install_permission, isAdmin), canManagement: hasPermission(permissions?.install_permission, isAdmin),
canDebugger: hasPermission(permissions?.debug_permission, isAdmin), canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
canSetPermissions: isAdmin, canSetPermissions: isAdmin,
permissions,
setPermissions: updatePermission,
isUpdatePending, isUpdatePending,
} }
} }
export const useCanInstallPluginFromMarketplace = () => { export const useCanInstallPluginFromMarketplace = () => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { canManagement } = usePermission() const { canManagement } = useReferenceSetting()
const canInstallPluginFromMarketplace = useMemo(() => { const canInstallPluginFromMarketplace = useMemo(() => {
return enable_marketplace && canManagement return enable_marketplace && canManagement
@ -58,4 +60,4 @@ export const useCanInstallPluginFromMarketplace = () => {
} }
} }
export default usePermission export default useReferenceSetting

@ -0,0 +1,9 @@
import type { AutoUpdateConfig } from './types'
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from './types'
export const defaultValue: AutoUpdateConfig = {
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
upgrade_time_of_day: 0,
upgrade_mode: AUTO_UPDATE_MODE.update_all,
exclude_plugins: [],
include_plugins: [],
}

@ -0,0 +1,185 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useMemo } from 'react'
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY, type AutoUpdateConfig } from './types'
import Label from '../label'
import StrategyPicker from './strategy-picker'
import { Trans, useTranslation } from 'react-i18next'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import PluginsPicker from './plugins-picker'
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from './utils'
import { useAppContext } from '@/context/app-context'
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
import { RiTimeLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
import { useModalContextSelector } from '@/context/modal-context'
const i18nPrefix = 'plugin.autoUpdate'
type Props = {
payload: AutoUpdateConfig
onChange: (payload: AutoUpdateConfig) => void
}
const SettingTimeZone: FC<{
children?: React.ReactNode
}> = ({
children,
}) => {
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
return (
<span className='body-xs-regular cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'language' })} >{children}</span>
)
}
const AutoUpdateSetting: FC<Props> = ({
payload,
onChange,
}) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const {
strategy_setting,
upgrade_time_of_day,
upgrade_mode,
exclude_plugins,
include_plugins,
} = payload
const minuteFilter = useCallback((minutes: string[]) => {
return minutes.filter((m) => {
const time = Number.parseInt(m, 10)
return time % 15 === 0
})
}, [])
const strategyDescription = useMemo(() => {
switch (strategy_setting) {
case AUTO_UPDATE_STRATEGY.fixOnly:
return t(`${i18nPrefix}.strategy.fixOnly.selectedDescription`)
case AUTO_UPDATE_STRATEGY.latest:
return t(`${i18nPrefix}.strategy.latest.selectedDescription`)
default:
return ''
}
}, [strategy_setting, t])
const plugins = useMemo(() => {
switch (upgrade_mode) {
case AUTO_UPDATE_MODE.partial:
return include_plugins
case AUTO_UPDATE_MODE.exclude:
return exclude_plugins
default:
return []
}
}, [upgrade_mode, exclude_plugins, include_plugins])
const handlePluginsChange = useCallback((newPlugins: string[]) => {
if (upgrade_mode === AUTO_UPDATE_MODE.partial) {
onChange({
...payload,
include_plugins: newPlugins,
})
}
else if (upgrade_mode === AUTO_UPDATE_MODE.exclude) {
onChange({
...payload,
exclude_plugins: newPlugins,
})
}
}, [payload, upgrade_mode, onChange])
const handleChange = useCallback((key: keyof AutoUpdateConfig) => {
return (value: AutoUpdateConfig[keyof AutoUpdateConfig]) => {
onChange({
...payload,
[key]: value,
})
}
}, [payload, onChange])
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
return (
<div
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
onClick={onClick}
>
<div className='flex w-0 grow items-center gap-x-1'>
<RiTimeLine className={cn(
'h-4 w-4 shrink-0 text-text-tertiary',
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
)} />
{inputElem}
</div>
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
</div>
)
}, [timezone])
return (
<div className='self-stretch px-6'>
<div className='my-3 flex items-center'>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.updateSettings`)}</div>
<div className='ml-2 h-px grow bg-divider-subtle'></div>
</div>
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<Label label={t(`${i18nPrefix}.automaticUpdates`)} description={strategyDescription} />
<StrategyPicker value={strategy_setting} onChange={handleChange('strategy_setting')} />
</div>
{strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
<>
<div className='flex items-center justify-between'>
<Label label={t(`${i18nPrefix}.updateTime`)} />
<div className='flex flex-col justify-start'>
<TimePicker
value={timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(upgrade_time_of_day, timezone!))}
timezone={timezone}
onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))}
onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))}
popupClassName='z-[99]'
title={t(`${i18nPrefix}.updateTime`)}
minuteFilter={minuteFilter}
renderTrigger={renderTimePickerTrigger}
/>
<div className='body-xs-regular mt-1 text-right text-text-tertiary'>
<Trans
i18nKey={`${i18nPrefix}.changeTimezone`}
components={{
setTimezone: <SettingTimeZone />,
}}
/>
</div>
</div>
</div>
<div>
<Label label={t(`${i18nPrefix}.specifyPluginsToUpdate`)} />
<div className='mt-1 flex w-full items-start justify-between gap-2'>
{[AUTO_UPDATE_MODE.update_all, AUTO_UPDATE_MODE.exclude, AUTO_UPDATE_MODE.partial].map(option => (
<OptionCard
key={option}
title={t(`${i18nPrefix}.upgradeMode.${option}`)}
onSelect={() => handleChange('upgrade_mode')(option)}
selected={upgrade_mode === option}
className="flex-1"
/>
))}
</div>
{upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
<PluginsPicker
value={plugins}
onChange={handlePluginsChange}
updateMode={upgrade_mode}
/>
)}
</div>
</>
)}
</div>
</div>
)
}
export default React.memo(AutoUpdateSetting)

@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
import { useTranslation } from 'react-i18next'
type Props = {
className: string
noPlugins?: boolean
}
const NoDataPlaceholder: FC<Props> = ({
className,
noPlugins,
}) => {
const { t } = useTranslation()
const icon = noPlugins ? (<Group className='size-6 text-text-quaternary' />) : (<SearchMenu className='size-8 text-text-tertiary' />)
const text = t(`plugin.autoUpdate.noPluginPlaceholder.${noPlugins ? 'noInstalled' : 'noFound'}`)
return (
<div className={cn('flex items-center justify-center', className)}>
<div className='flex flex-col items-center'>
{icon}
<div className='system-sm-regular mt-2 text-text-tertiary'>{text}</div>
</div>
</div>
)
}
export default React.memo(NoDataPlaceholder)

@ -0,0 +1,22 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { AUTO_UPDATE_MODE } from './types'
import { useTranslation } from 'react-i18next'
type Props = {
updateMode: AUTO_UPDATE_MODE
}
const NoPluginSelected: FC<Props> = ({
updateMode,
}) => {
const { t } = useTranslation()
const text = `${t(`plugin.autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`)}`
return (
<div className='system-xs-regular rounded-[10px] border border-[divider-subtle] bg-background-section p-3 text-center text-text-tertiary'>
{text}
</div>
)
}
export default React.memo(NoPluginSelected)

@ -0,0 +1,69 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import NoPluginSelected from './no-plugin-selected'
import { AUTO_UPDATE_MODE } from './types'
import PluginsSelected from './plugins-selected'
import Button from '@/app/components/base/button'
import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import ToolPicker from './tool-picker'
const i18nPrefix = 'plugin.autoUpdate'
type Props = {
updateMode: AUTO_UPDATE_MODE
value: string[] // plugin ids
onChange: (value: string[]) => void
}
const PluginsPicker: FC<Props> = ({
updateMode,
value,
onChange,
}) => {
const { t } = useTranslation()
const hasSelected = value.length > 0
const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude
const handleClear = () => {
onChange([])
}
const [isShowToolPicker, {
set: setToolPicker,
}] = useBoolean(false)
return (
<div className='mt-2 rounded-[10px] bg-background-section-burn p-2.5'>
{hasSelected ? (
<div className='flex justify-between text-text-tertiary'>
<div className='system-xs-medium'>{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { num: value.length })}</div>
<div className='system-xs-medium cursor-pointer' onClick={handleClear}>{t(`${i18nPrefix}.operation.clearAll`)}</div>
</div>
) : (
<NoPluginSelected updateMode={updateMode} />
)}
{hasSelected && (
<PluginsSelected
className='mt-2'
plugins={value}
/>
)}
<ToolPicker
trigger={
<Button className='mt-2 w-[412px]' size='small' variant='secondary-accent'>
<RiAddLine className='size-3.5' />
{t(`${i18nPrefix}.operation.select`)}
</Button>
}
value={value}
onChange={onChange}
isShow={isShowToolPicker}
onShowChange={setToolPicker}
/>
</div>
)
}
export default React.memo(PluginsPicker)

@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Icon from '@/app/components/plugins/card/base/card-icon'
const MAX_DISPLAY_COUNT = 14
type Props = {
className?: string
plugins: string[]
}
const PluginsSelected: FC<Props> = ({
className,
plugins,
}) => {
const isShowAll = plugins.length < MAX_DISPLAY_COUNT
const displayPlugins = plugins.slice(0, MAX_DISPLAY_COUNT)
return (
<div className={cn('flex items-center space-x-1', className)}>
{displayPlugins.map(plugin => (
<Icon key={plugin} size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} />
))}
{!isShowAll && <div className='system-xs-medium text-text-tertiary'>+{plugins.length - MAX_DISPLAY_COUNT}</div>}
</div>
)
}
export default React.memo(PluginsSelected)

@ -0,0 +1,98 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import { AUTO_UPDATE_STRATEGY } from './types'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
const i18nPrefix = 'plugin.autoUpdate.strategy'
type Props = {
value: AUTO_UPDATE_STRATEGY
onChange: (value: AUTO_UPDATE_STRATEGY) => void
}
const StrategyPicker = ({
value,
onChange,
}: Props) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const options = [
{
value: AUTO_UPDATE_STRATEGY.disabled,
label: t(`${i18nPrefix}.disabled.name`),
description: t(`${i18nPrefix}.disabled.description`),
},
{
value: AUTO_UPDATE_STRATEGY.fixOnly,
label: t(`${i18nPrefix}.fixOnly.name`),
description: t(`${i18nPrefix}.fixOnly.description`),
},
{
value: AUTO_UPDATE_STRATEGY.latest,
label: t(`${i18nPrefix}.latest.name`),
description: t(`${i18nPrefix}.latest.description`),
},
]
const selectedOption = options.find(option => option.value === value)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='top-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setOpen(v => !v)
}}>
<Button
size='small'
>
{selectedOption?.label}
<RiArrowDownSLine className='h-3.5 w-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[99]'>
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
{
options.map(option => (
<div
key={option.value}
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onChange(option.value)
setOpen(false)
}}
>
<div className='mr-1 w-4 shrink-0'>
{
value === option.value && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
<div className='grow'>
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
</div>
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default StrategyPicker

@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { renderI18nObject } from '@/i18n'
import { useGetLanguage } from '@/context/i18n'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Checkbox from '@/app/components/base/checkbox'
type Props = {
payload: PluginDetail
isChecked?: boolean
onCheckChange: () => void
}
const ToolItem: FC<Props> = ({
payload,
isChecked,
onCheckChange,
}) => {
const language = useGetLanguage()
const { plugin_id, declaration } = payload
const { label, author: org } = declaration
return (
<div className='p-1'>
<div
className='flex w-full select-none items-center rounded-lg pr-2 hover:bg-state-base-hover'
>
<div className='flex h-8 grow items-center space-x-2 pl-3 pr-2'>
<Icon size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} />
<div className='system-sm-medium max-w-[150px] shrink-0 truncate text-text-primary'>{renderI18nObject(label, language)}</div>
<div className='system-xs-regular max-w-[150px] shrink-0 truncate text-text-quaternary'>{org}</div>
</div>
<Checkbox
checked={isChecked}
onCheck={onCheckChange}
className='shrink-0'
/>
</div>
</div>
)
}
export default React.memo(ToolItem)

@ -0,0 +1,165 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useInstalledPluginList } from '@/service/use-plugins'
import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import ToolItem from './tool-item'
import Loading from '@/app/components/base/loading'
import NoDataPlaceholder from './no-data-placeholder'
type Props = {
trigger: React.ReactNode
value: string[]
onChange: (value: string[]) => void
isShow: boolean
onShowChange: (isShow: boolean) => void
}
const ToolPicker: FC<Props> = ({
trigger,
value,
onChange,
isShow,
onShowChange,
}) => {
const { t } = useTranslation()
const toggleShowPopup = useCallback(() => {
onShowChange(!isShow)
}, [onShowChange, isShow])
const tabs = [
{
key: PLUGIN_TYPE_SEARCH_MAP.all,
name: t('plugin.category.all'),
},
{
key: PLUGIN_TYPE_SEARCH_MAP.model,
name: t('plugin.category.models'),
},
{
key: PLUGIN_TYPE_SEARCH_MAP.tool,
name: t('plugin.category.tools'),
},
{
key: PLUGIN_TYPE_SEARCH_MAP.agent,
name: t('plugin.category.agents'),
},
{
key: PLUGIN_TYPE_SEARCH_MAP.extension,
name: t('plugin.category.extensions'),
},
{
key: PLUGIN_TYPE_SEARCH_MAP.bundle,
name: t('plugin.category.bundles'),
},
]
const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
const [query, setQuery] = useState('')
const [tags, setTags] = useState<string[]>([])
const { data, isLoading } = useInstalledPluginList()
const filteredList = useMemo(() => {
const list = data ? data.plugins : []
return list.filter((plugin) => {
return (
(pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
&& (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
)
})
}, [data, pluginType, query, tags])
const handleCheckChange = useCallback((pluginId: string) => {
return () => {
const newValue = value.includes(pluginId)
? value.filter(id => id !== pluginId)
: [...value, pluginId]
onChange(newValue)
}
}, [onChange, value])
const listContent = (
<div className='max-h-[396px] overflow-y-auto'>
{filteredList.map(item => (
<ToolItem
key={item.plugin_id}
payload={item}
isChecked={value.includes(item.plugin_id)}
onCheckChange={handleCheckChange(item.plugin_id)}
/>
))}
</div>
)
const loadingContent = (
<div className='flex h-[396px] items-center justify-center'>
<Loading />
</div>
)
const noData = (
<NoDataPlaceholder className='h-[396px]' noPlugins={!query} />
)
return (
<PortalToFollowElem
placement='top'
offset={0}
open={isShow}
onOpenChange={onShowChange}
>
<PortalToFollowElemTrigger
onClick={toggleShowPopup}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}>
<div className='p-2 pb-1'>
<SearchBox
search={query}
onSearchChange={setQuery}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
inputClassName='w-full'
/>
</div>
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
<div className='flex h-8 items-center space-x-1'>
{
tabs.map(tab => (
<div
className={cn(
'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
'text-xs font-medium text-text-secondary',
pluginType === tab.key && 'bg-state-base-hover-alt',
)}
key={tab.key}
onClick={() => setPluginType(tab.key)}
>
{tab.name}
</div>
))
}
</div>
</div>
{!isLoading && filteredList.length > 0 && listContent}
{!isLoading && filteredList.length === 0 && noData}
{isLoading && loadingContent}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(ToolPicker)

@ -0,0 +1,19 @@
export enum AUTO_UPDATE_STRATEGY {
fixOnly = 'fix_only',
disabled = 'disabled',
latest = 'latest',
}
export enum AUTO_UPDATE_MODE {
partial = 'partial',
exclude = 'exclude',
update_all = 'all',
}
export type AutoUpdateConfig = {
strategy_setting: AUTO_UPDATE_STRATEGY
upgrade_time_of_day: number
upgrade_mode: AUTO_UPDATE_MODE
exclude_plugins: string[]
include_plugins: string[]
}

@ -0,0 +1,14 @@
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds } from './utils'
describe('convertLocalSecondsToUTCDaySeconds', () => {
it('should convert local seconds to UTC day seconds correctly', () => {
const localTimezone = 'Asia/Shanghai'
const utcSeconds = convertLocalSecondsToUTCDaySeconds(0, localTimezone)
expect(utcSeconds).toBe((24 - 8) * 3600)
})
it('should convert local seconds to UTC day seconds for a specific time', () => {
const localTimezone = 'Asia/Shanghai'
expect(convertUTCDaySecondsToLocalSeconds(convertLocalSecondsToUTCDaySeconds(0, localTimezone), localTimezone)).toBe(0)
})
})

@ -0,0 +1,37 @@
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc)
dayjs.extend(timezone)
export const timeOfDayToDayjs = (timeOfDay: number): Dayjs => {
const hours = Math.floor(timeOfDay / 3600)
const minutes = (timeOfDay - hours * 3600) / 60
const res = dayjs().startOf('day').hour(hours).minute(minutes)
return res
}
export const convertLocalSecondsToUTCDaySeconds = (secondsInDay: number, localTimezone: string): number => {
const localDayStart = dayjs().tz(localTimezone).startOf('day')
const localTargetTime = localDayStart.add(secondsInDay, 'second')
const utcTargetTime = localTargetTime.utc()
const utcDayStart = utcTargetTime.startOf('day')
const secondsFromUTCMidnight = utcTargetTime.diff(utcDayStart, 'second')
return secondsFromUTCMidnight
}
export const dayjsToTimeOfDay = (date?: Dayjs): number => {
if (!date) return 0
return date.hour() * 3600 + date.minute() * 60
}
export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => {
const utcDayStart = dayjs().utc().startOf('day')
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
const localTargetTime = utcTargetTime.tz(localTimezone)
const localDayStart = localTargetTime.startOf('day')
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
return secondsInLocalDay
}

@ -0,0 +1,28 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
label: string
description?: string
}
const Label: FC<Props> = ({
label,
description,
}) => {
return (
<div>
<div className={cn('flex h-6 items-center', description && 'h-4')}>
<span className='system-sm-semibold text-text-secondary'>{label}</span>
</div>
{description && (
<div className='body-xs-regular mt-1 text-text-tertiary'>
{description}
</div>
)}
</div>
)
}
export default React.memo(Label)

@ -5,14 +5,18 @@ import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { Permissions } from '@/app/components/plugins/types' import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
import { PermissionType } from '@/app/components/plugins/types' import { PermissionType } from '@/app/components/plugins/types'
import type { AutoUpdateConfig } from './auto-update-setting/types'
import AutoUpdateSetting from './auto-update-setting'
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
import Label from './label'
const i18nPrefix = 'plugin.privilege' const i18nPrefix = 'plugin.privilege'
type Props = { type Props = {
payload: Permissions payload: ReferenceSetting
onHide: () => void onHide: () => void
onSave: (payload: Permissions) => void onSave: (payload: ReferenceSetting) => void
} }
const PluginSettingModal: FC<Props> = ({ const PluginSettingModal: FC<Props> = ({
@ -21,7 +25,9 @@ const PluginSettingModal: FC<Props> = ({
onSave, onSave,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(payload) const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
const handlePrivilegeChange = useCallback((key: string) => { const handlePrivilegeChange = useCallback((key: string) => {
return (value: PermissionType) => { return (value: PermissionType) => {
setTempPrivilege({ setTempPrivilege({
@ -32,18 +38,21 @@ const PluginSettingModal: FC<Props> = ({
}, [tempPrivilege]) }, [tempPrivilege])
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
await onSave(tempPrivilege) await onSave({
permission: tempPrivilege,
auto_upgrade: tempAutoUpdateConfig,
})
onHide() onHide()
}, [onHide, onSave, tempPrivilege]) }, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
return ( return (
<Modal <Modal
isShow isShow
onClose={onHide} onClose={onHide}
closable closable
className='w-[420px] !p-0' className='w-[480px] !p-0'
> >
<div className='shadows-shadow-xl flex w-[420px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'> <div className='shadows-shadow-xl flex w-[480px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'>
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'> <div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
<span className='title-2xl-semi-bold self-stretch text-text-primary'>{t(`${i18nPrefix}.title`)}</span> <span className='title-2xl-semi-bold self-stretch text-text-primary'>{t(`${i18nPrefix}.title`)}</span>
</div> </div>
@ -53,9 +62,7 @@ const PluginSettingModal: FC<Props> = ({
{ title: t(`${i18nPrefix}.whoCanDebug`), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne }, { title: t(`${i18nPrefix}.whoCanDebug`), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
].map(({ title, key, value }) => ( ].map(({ title, key, value }) => (
<div key={key} className='flex flex-col items-start gap-1 self-stretch'> <div key={key} className='flex flex-col items-start gap-1 self-stretch'>
<div className='flex h-6 items-center gap-0.5'> <Label label={title} />
<span className='system-sm-semibold text-text-secondary'>{title}</span>
</div>
<div className='flex w-full items-start justify-between gap-2'> <div className='flex w-full items-start justify-between gap-2'>
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => ( {[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
<OptionCard <OptionCard
@ -70,6 +77,8 @@ const PluginSettingModal: FC<Props> = ({
</div> </div>
))} ))}
</div> </div>
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
<div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'> <div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'>
<Button <Button
className='min-w-[72px]' className='min-w-[72px]'

@ -2,6 +2,7 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
import type { ToolCredential } from '@/app/components/tools/types' import type { ToolCredential } from '@/app/components/tools/types'
import type { Locale } from '@/i18n' import type { Locale } from '@/i18n'
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types' import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
export enum PluginType { export enum PluginType {
tool = 'tool', tool = 'tool',
model = 'model', model = 'model',
@ -167,6 +168,11 @@ export type Permissions = {
debug_permission: PermissionType debug_permission: PermissionType
} }
export type ReferenceSetting = {
permission: Permissions
auto_upgrade: AutoUpdateConfig
}
export type UpdateFromMarketPlacePayload = { export type UpdateFromMarketPlacePayload = {
category: PluginType category: PluginType
originalPackageInfo: { originalPackageInfo: {

@ -0,0 +1,35 @@
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
const i18nPrefix = 'plugin.autoUpdate.pluginDowngradeWarning'
type Props = {
onCancel: () => void
onJustDowngrade: () => void
onExcludeAndDowngrade: () => void
}
const DowngradeWarningModal = ({
onCancel,
onJustDowngrade,
onExcludeAndDowngrade,
}: Props) => {
const { t } = useTranslation()
return (
<>
<div className='flex flex-col items-start gap-2 self-stretch'>
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
<div className='system-md-regular text-text-secondary'>
{t(`${i18nPrefix}.description`)}
</div>
</div>
<div className='mt-9 flex items-start justify-end space-x-2 self-stretch'>
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
<Button variant='secondary' destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`)}</Button>
<Button variant='primary' onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`)}</Button>
</div>
</>
)
}
export default DowngradeWarningModal

@ -13,13 +13,18 @@ import { updateFromMarketPlace } from '@/service/plugins'
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status' import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
import { usePluginTaskList } from '@/service/use-plugins' import { usePluginTaskList } from '@/service/use-plugins'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import DowngradeWarningModal from './downgrade-warning'
import { useInvalidateReferenceSettings, useRemoveAutoUpgrade } from '@/service/use-plugins'
import cn from '@/utils/classnames'
const i18nPrefix = 'plugin.upgrade' const i18nPrefix = 'plugin.upgrade'
type Props = { type Props = {
payload: UpdateFromMarketPlacePayload payload: UpdateFromMarketPlacePayload
pluginId: string
onSave: () => void onSave: () => void
onCancel: () => void onCancel: () => void
isShowDowngradeWarningModal?: boolean
} }
enum UploadStep { enum UploadStep {
@ -30,8 +35,10 @@ enum UploadStep {
const UpdatePluginModal: FC<Props> = ({ const UpdatePluginModal: FC<Props> = ({
payload, payload,
pluginId,
onSave, onSave,
onCancel, onCancel,
isShowDowngradeWarningModal,
}) => { }) => {
const { const {
originalPackageInfo, originalPackageInfo,
@ -103,51 +110,74 @@ const UpdatePluginModal: FC<Props> = ({
onSave() onSave()
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id]) }, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
const { mutateAsync } = useRemoveAutoUpgrade()
const invalidateReferenceSettings = useInvalidateReferenceSettings()
const handleExcludeAndDownload = async () => {
await mutateAsync({
plugin_id: pluginId,
})
invalidateReferenceSettings()
handleConfirm()
}
const doShowDowngradeWarningModal = isShowDowngradeWarningModal && uploadStep === UploadStep.notStarted
return ( return (
<Modal <Modal
isShow={true} isShow={true}
onClose={onCancel} onClose={onCancel}
className='min-w-[560px]' className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')}
closable closable
title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)} title={!doShowDowngradeWarningModal && t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
> >
<div className='system-md-regular mb-2 mt-3 text-text-secondary'> {doShowDowngradeWarningModal && (
{t(`${i18nPrefix}.description`)} <DowngradeWarningModal
</div> onCancel={onCancel}
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'> onJustDowngrade={handleConfirm}
<Card onExcludeAndDowngrade={handleExcludeAndDownload}
installed={uploadStep === UploadStep.installed}
payload={pluginManifestToCardPluginProps({
...originalPackageInfo.payload,
icon: icon!,
})}
className='w-full'
titleLeft={
<>
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
</Badge>
</>
}
/> />
</div> )}
<div className='flex items-center justify-end gap-2 self-stretch pt-5'> {!doShowDowngradeWarningModal && (
{uploadStep === UploadStep.notStarted && ( <>
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
{t(`${i18nPrefix}.description`)}
</div>
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
<Card
installed={uploadStep === UploadStep.installed}
payload={pluginManifestToCardPluginProps({
...originalPackageInfo.payload,
icon: icon!,
})}
className='w-full'
titleLeft={
<>
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
</Badge>
</>
}
/>
</div>
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
{uploadStep === UploadStep.notStarted && (
<Button
onClick={handleCancel}
>
{t('common.operation.cancel')}
</Button>
)}
<Button <Button
onClick={handleCancel} variant='primary'
loading={uploadStep === UploadStep.upgrading}
onClick={handleConfirm}
disabled={uploadStep === UploadStep.upgrading}
> >
{t('common.operation.cancel')} {configBtnText}
</Button> </Button>
)} </div>
<Button </>
variant='primary' )}
loading={uploadStep === UploadStep.upgrading}
onClick={handleConfirm}
disabled={uploadStep === UploadStep.upgrading}
>
{configBtnText}
</Button>
</div>
</Modal> </Modal>
) )
} }

@ -15,6 +15,7 @@ import type {
import { useVersionListOfPlugin } from '@/service/use-plugins' import { useVersionListOfPlugin } from '@/service/use-plugins'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { lt } from 'semver'
type Props = { type Props = {
disabled?: boolean disabled?: boolean
@ -28,9 +29,11 @@ type Props = {
onSelect: ({ onSelect: ({
version, version,
unique_identifier, unique_identifier,
isDowngrade,
}: { }: {
version: string version: string
unique_identifier: string unique_identifier: string
isDowngrade: boolean
}) => void }) => void
} }
@ -59,13 +62,14 @@ const PluginVersionPicker: FC<Props> = ({
const { data: res } = useVersionListOfPlugin(pluginID) const { data: res } = useVersionListOfPlugin(pluginID)
const handleSelect = useCallback(({ version, unique_identifier }: { const handleSelect = useCallback(({ version, unique_identifier, isDowngrade }: {
version: string version: string
unique_identifier: string unique_identifier: string
isDowngrade: boolean
}) => { }) => {
if (currentVersion === version) if (currentVersion === version)
return return
onSelect({ version, unique_identifier }) onSelect({ version, unique_identifier, isDowngrade })
onShowChange(false) onShowChange(false)
}, [currentVersion, onSelect, onShowChange]) }, [currentVersion, onSelect, onShowChange])
@ -99,6 +103,7 @@ const PluginVersionPicker: FC<Props> = ({
onClick={() => handleSelect({ onClick={() => handleSelect({
version: version.version, version: version.version,
unique_identifier: version.unique_identifier, unique_identifier: version.unique_identifier,
isDowngrade: lt(version.version, currentVersion),
})} })}
> >
<div className='flex grow items-center'> <div className='flex grow items-center'>

@ -114,6 +114,56 @@ const translation = {
admins: 'Admins', admins: 'Admins',
noone: 'No one', noone: 'No one',
}, },
autoUpdate: {
automaticUpdates: 'Automatic updates',
updateTime: 'Update time',
specifyPluginsToUpdate: 'Specify plugins to update',
strategy: {
disabled: {
name: 'Disabled',
description: 'Plugins will not auto-update',
},
fixOnly: {
name: 'Fix Only',
description: 'Auto-update for patch versions only (e.g., 1.0.1 → 1.0.2). Minor version changes won\'t trigger updates.',
selectedDescription: 'Auto-update for patch versions only',
},
latest: {
name: 'Latest',
description: 'Always update to latest version',
selectedDescription: 'Always update to latest version',
},
},
updateTimeTitle: 'Update time',
upgradeMode: {
all: 'Update all',
exclude: 'Exclude selected',
partial: 'Only selected',
},
upgradeModePlaceholder: {
exclude: 'Selected plugins will not auto-update',
partial: 'Only selected plugins will auto-update. No plugins are currently selected, so no plugins will auto-update.',
},
excludeUpdate: 'The following {{num}} plugins will not auto-update',
partialUPdate: 'Only the following {{num}} plugins will auto-update',
operation: {
clearAll: 'Clear all',
select: 'Select plugins',
},
nextUpdateTime: 'Next auto-update: {{time}}',
pluginDowngradeWarning: {
title: 'Plugin Downgrade',
description: 'Auto-update is currently enabled for this plugin. Downgrading the version may cause your changes to be overwritten during the next automatic update.',
downgrade: 'Downgrade anyway',
exclude: 'Exclude from auto-update',
},
noPluginPlaceholder: {
noFound: 'No plugins were found',
noInstalled: 'No plugins installed',
},
updateSettings: 'Update Settings',
changeTimezone: 'To change time zone, go to <setTimezone>Settings</setTimezone>',
},
pluginInfoModal: { pluginInfoModal: {
title: 'Plugin info', title: 'Plugin info',
repository: 'Repository', repository: 'Repository',

@ -114,6 +114,56 @@ const translation = {
admins: '管理员', admins: '管理员',
noone: '无人', noone: '无人',
}, },
autoUpdate: {
automaticUpdates: '自动更新',
updateTime: '更新时间',
specifyPluginsToUpdate: '指定要更新的插件',
strategy: {
disabled: {
name: '禁用',
description: '插件将不会自动更新',
},
fixOnly: {
name: '仅修复',
description: '仅自动更新补丁版本例如1.0.1 → 1.0.2)。次要版本更改不会触发更新。',
selectedDescription: '仅自动更新补丁版本',
},
latest: {
name: '最新',
description: '始终更新到最新版本',
selectedDescription: '始终更新到最新版本',
},
},
updateTimeTitle: '更新时间',
upgradeMode: {
all: '更新全部',
exclude: '排除选定',
partial: '仅选定',
},
upgradeModePlaceholder: {
exclude: '选定的插件将不会自动更新',
partial: '仅选定的插件将自动更新。目前未选择任何插件,因此不会自动更新任何插件。',
},
excludeUpdate: '以下 {{num}} 个插件将不会自动更新',
partialUPdate: '仅以下 {{num}} 个插件将自动更新',
operation: {
clearAll: '清除所有',
select: '选择插件',
},
nextUpdateTime: '下次自动更新时间: {{time}}',
pluginDowngradeWarning: {
title: '插件降级',
description: '此插件目前已启用自动更新。降级版本可能会导致您的更改在下次自动更新时被覆盖。',
downgrade: '仍然降级',
exclude: '从自动更新中排除',
},
noPluginPlaceholder: {
noFound: '未找到插件',
noInstalled: '未安装插件',
},
updateSettings: '更新设置',
changeTimezone: '要更改时区,请前往<setTimezone>设置</setTimezone>',
},
pluginInfoModal: { pluginInfoModal: {
title: '插件信息', title: '插件信息',
repository: '仓库', repository: '仓库',

@ -13,7 +13,6 @@ import type {
InstalledLatestVersionResponse, InstalledLatestVersionResponse,
InstalledPluginListWithTotalResponse, InstalledPluginListWithTotalResponse,
PackageDependency, PackageDependency,
Permissions,
Plugin, Plugin,
PluginDeclaration, PluginDeclaration,
PluginDetail, PluginDetail,
@ -22,6 +21,7 @@ import type {
PluginType, PluginType,
PluginsFromMarketplaceByInfoResponse, PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse, PluginsFromMarketplaceResponse,
ReferenceSetting,
VersionInfo, VersionInfo,
VersionListResponse, VersionListResponse,
uploadGitHubResponse, uploadGitHubResponse,
@ -40,7 +40,7 @@ import {
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import { useInvalidateAllBuiltInTools } from './use-tools' import { useInvalidateAllBuiltInTools } from './use-tools'
import usePermission from '@/app/components/plugins/plugin-page/use-permission' import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting'
import { uninstallPlugin } from '@/service/plugins' import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
@ -350,37 +350,45 @@ export const useDebugKey = () => {
}) })
} }
const usePermissionsKey = [NAME_SPACE, 'permissions'] const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
export const usePermissions = () => { export const useReferenceSettings = () => {
return useQuery({ return useQuery({
queryKey: usePermissionsKey, queryKey: useReferenceSettingKey,
queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'), queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'),
}) })
} }
export const useInvalidatePermissions = () => { export const useInvalidateReferenceSettings = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return () => { return () => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
{ {
queryKey: usePermissionsKey, queryKey: useReferenceSettingKey,
}) })
} }
} }
export const useMutationPermissions = ({ export const useMutationReferenceSettings = ({
onSuccess, onSuccess,
}: { }: {
onSuccess?: () => void onSuccess?: () => void
}) => { }) => {
return useMutation({ return useMutation({
mutationFn: (payload: Permissions) => { mutationFn: (payload: ReferenceSetting) => {
return post('/workspaces/current/plugin/permission/change', { body: payload }) return post('/workspaces/current/plugin/preferences/change', { body: payload })
}, },
onSuccess, onSuccess,
}) })
} }
export const useRemoveAutoUpgrade = () => {
return useMutation({
mutationFn: (payload: { plugin_id: string }) => {
return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload })
},
})
}
export const useMutationPluginsFromMarketplace = () => { export const useMutationPluginsFromMarketplace = () => {
return useMutation({ return useMutation({
mutationFn: (pluginsSearchParams: PluginsSearchParams) => { mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
@ -427,6 +435,39 @@ export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[],
}) })
} }
export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearchParams) => {
return useQuery({
queryKey: [NAME_SPACE, 'fetchPluginListOrBundleList', pluginsSearchParams],
queryFn: () => {
const {
query,
sortBy,
sortOrder,
category,
tags,
exclude,
type,
page = 1,
pageSize = 40,
} = pluginsSearchParams
const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
body: {
page,
page_size: pageSize,
query,
sort_by: sortBy,
sort_order: sortOrder,
category: category !== 'all' ? category : '',
tags,
exclude,
type,
},
})
},
})
}
export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => { export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos], queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
@ -448,7 +489,7 @@ const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
export const usePluginTaskList = (category?: PluginType) => { export const usePluginTaskList = (category?: PluginType) => {
const { const {
canManagement, canManagement,
} = usePermission() } = useReferenceSetting()
const { refreshPluginList } = useRefreshPluginList() const { refreshPluginList } = useRefreshPluginList()
const { const {
data, data,

Loading…
Cancel
Save