浏览代码

feat: plugin auto upgrade strategy (#19758)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Novice <novice12185727@gmail.com>
tags/1.7.0
Junyan Qin (Chin) 3 个月前
父节点
当前提交
eaae79a581
没有帐户链接到提交者的电子邮件
共有 100 个文件被更改,包括 1094 次插入322 次删除
  1. 10
    0
      api/.env.example
  2. 6
    1
      api/README.md
  3. 36
    0
      api/configs/feature/__init__.py
  4. 114
    1
      api/controllers/console/workspace/plugin.py
  5. 20
    0
      api/core/helper/marketplace.py
  6. 1
    1
      api/docker/entrypoint.sh
  7. 39
    26
      api/extensions/ext_celery.py
  8. 42
    0
      api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py
  9. 37
    3
      api/models/account.py
  10. 49
    0
      api/schedule/check_upgradable_plugin_task.py
  11. 12
    0
      api/services/account_service.py
  12. 87
    0
      api/services/plugin/plugin_auto_upgrade_service.py
  13. 166
    0
      api/tasks/process_tenant_plugin_autoupgrade_check_task.py
  14. 10
    0
      docker/.env.example
  15. 19
    0
      docker/docker-compose-template.yaml
  16. 27
    0
      docker/docker-compose.yaml
  17. 4
    1
      web/app/components/base/date-and-time-picker/common/option-list-item.tsx
  18. 7
    2
      web/app/components/base/date-and-time-picker/time-picker/header.tsx
  19. 21
    10
      web/app/components/base/date-and-time-picker/time-picker/index.tsx
  20. 3
    1
      web/app/components/base/date-and-time-picker/time-picker/options.tsx
  21. 11
    1
      web/app/components/base/date-and-time-picker/types.ts
  22. 12
    0
      web/app/components/base/date-and-time-picker/utils/dayjs.ts
  23. 7
    0
      web/app/components/base/icons/assets/vender/line/general/search-menu.svg
  24. 4
    0
      web/app/components/base/icons/assets/vender/system/auto-update-line.svg
  25. 1
    1
      web/app/components/base/icons/script.mjs
  26. 129
    116
      web/app/components/base/icons/src/public/tracing/AliyunIcon.json
  27. 9
    5
      web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx
  28. 115
    69
      web/app/components/base/icons/src/public/tracing/AliyunIconBig.json
  29. 9
    5
      web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx
  30. 9
    5
      web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx
  31. 9
    5
      web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx
  32. 2
    2
      web/app/components/base/icons/src/public/tracing/index.ts
  33. 1
    1
      web/app/components/base/icons/src/vender/features/Citations.json
  34. 1
    1
      web/app/components/base/icons/src/vender/features/ContentModeration.json
  35. 1
    1
      web/app/components/base/icons/src/vender/features/Document.json
  36. 1
    1
      web/app/components/base/icons/src/vender/features/FolderUpload.json
  37. 1
    1
      web/app/components/base/icons/src/vender/features/LoveMessage.json
  38. 1
    1
      web/app/components/base/icons/src/vender/features/MessageFast.json
  39. 1
    1
      web/app/components/base/icons/src/vender/features/Microphone01.json
  40. 1
    1
      web/app/components/base/icons/src/vender/features/TextToAudio.json
  41. 1
    1
      web/app/components/base/icons/src/vender/features/VirtualAssistant.json
  42. 1
    1
      web/app/components/base/icons/src/vender/features/Vision.json
  43. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json
  44. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json
  45. 1
    1
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json
  46. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json
  47. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json
  48. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json
  49. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json
  50. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json
  51. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json
  52. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json
  53. 1
    1
      web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json
  54. 1
    1
      web/app/components/base/icons/src/vender/line/communication/AiText.json
  55. 1
    1
      web/app/components/base/icons/src/vender/line/communication/ChatBot.json
  56. 1
    1
      web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json
  57. 1
    1
      web/app/components/base/icons/src/vender/line/communication/CuteRobot.json
  58. 1
    1
      web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json
  59. 1
    1
      web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json
  60. 1
    1
      web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json
  61. 1
    1
      web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json
  62. 1
    1
      web/app/components/base/icons/src/vender/line/development/BracketsX.json
  63. 1
    1
      web/app/components/base/icons/src/vender/line/development/CodeBrowser.json
  64. 1
    1
      web/app/components/base/icons/src/vender/line/development/Container.json
  65. 1
    1
      web/app/components/base/icons/src/vender/line/development/Database01.json
  66. 1
    1
      web/app/components/base/icons/src/vender/line/development/Database03.json
  67. 1
    1
      web/app/components/base/icons/src/vender/line/development/FileHeart02.json
  68. 1
    1
      web/app/components/base/icons/src/vender/line/development/GitBranch01.json
  69. 1
    1
      web/app/components/base/icons/src/vender/line/development/PromptEngineering.json
  70. 1
    1
      web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json
  71. 1
    1
      web/app/components/base/icons/src/vender/line/development/TerminalSquare.json
  72. 1
    1
      web/app/components/base/icons/src/vender/line/development/Variable.json
  73. 1
    1
      web/app/components/base/icons/src/vender/line/development/Webhooks.json
  74. 1
    1
      web/app/components/base/icons/src/vender/line/editor/AlignLeft.json
  75. 1
    1
      web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json
  76. 1
    1
      web/app/components/base/icons/src/vender/line/editor/Collapse.json
  77. 1
    1
      web/app/components/base/icons/src/vender/line/editor/Colors.json
  78. 1
    1
      web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json
  79. 1
    1
      web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json
  80. 1
    1
      web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json
  81. 1
    1
      web/app/components/base/icons/src/vender/line/editor/TypeSquare.json
  82. 1
    1
      web/app/components/base/icons/src/vender/line/education/BookOpen01.json
  83. 1
    1
      web/app/components/base/icons/src/vender/line/files/File02.json
  84. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileArrow01.json
  85. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileCheck02.json
  86. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileDownload02.json
  87. 1
    1
      web/app/components/base/icons/src/vender/line/files/FilePlus01.json
  88. 1
    1
      web/app/components/base/icons/src/vender/line/files/FilePlus02.json
  89. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileText.json
  90. 1
    1
      web/app/components/base/icons/src/vender/line/files/FileUpload.json
  91. 1
    1
      web/app/components/base/icons/src/vender/line/files/Folder.json
  92. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json
  93. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json
  94. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json
  95. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json
  96. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json
  97. 1
    1
      web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json
  98. 1
    1
      web/app/components/base/icons/src/vender/line/general/AtSign.json
  99. 1
    1
      web/app/components/base/icons/src/vender/line/general/Bookmark.json
  100. 0
    0
      web/app/components/base/icons/src/vender/line/general/Check.json

+ 10
- 0
api/.env.example 查看文件

@@ -471,6 +471,16 @@ APP_MAX_ACTIVE_REQUESTS=0
# Celery beat configuration
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_TOOL_PINS=
POSITION_TOOL_INCLUDES=

+ 6
- 1
api/README.md 查看文件

@@ -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.

```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

+ 36
- 0
api/configs/feature/__init__.py 查看文件

@@ -832,6 +832,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):
POSITION_PROVIDER_PINS: str = Field(
description="Comma-separated list of pinned model providers",
@@ -961,5 +996,6 @@ class FeatureConfig(
# hosted services config
HostedServiceConfig,
CeleryBeatConfig,
CeleryScheduleTasksConfig,
):
pass

+ 114
- 1
api/controllers/console/workspace/plugin.py 查看文件

@@ -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.plugin.impl.exc import PluginDaemonClientSideError
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_permission_service import PluginPermissionService
from services.plugin.plugin_service import PluginService
@@ -534,6 +535,114 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
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(PluginListApi, "/workspaces/current/plugin/list")
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(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")

+ 20
- 0
api/core/helper/marketplace.py 查看文件

@@ -25,9 +25,29 @@ def batch_fetch_plugin_manifests(plugin_ids: list[str]) -> Sequence[MarketplaceP
url = str(marketplace_api_url / "api/v1/plugins/batch")
response = requests.post(url, json={"plugin_ids": plugin_ids})
response.raise_for_status()

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):
url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
response = requests.post(url, json={"unique_identifier": plugin_unique_identifier})

+ 1
- 1
api/docker/entrypoint.sh 查看文件

@@ -22,7 +22,7 @@ if [[ "${MODE}" == "worker" ]]; then

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} \
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion}
-Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion,plugin}

elif [[ "${MODE}" == "beat" ]]; then
exec celery -A app.celery beat --loglevel ${LOG_LEVEL:-INFO}

+ 39
- 26
api/extensions/ext_celery.py 查看文件

@@ -64,49 +64,62 @@ def init_app(app: DifyApp) -> Celery:
celery_app.set_default()
app.extensions["celery"] = celery_app

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",
]
imports = []
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",
"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",
"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",
"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",
"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",
"schedule": timedelta(days=day),
},
# every Monday
"mail_clean_document_notify_task": {
}
if dify_config.ENABLE_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",
"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",
"schedule": timedelta(
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)

return celery_app

+ 42
- 0
api/migrations/versions/2025_07_23_1508-8bcc02c9bd07_add_tenant_plugin_autoupgrade_table.py 查看文件

@@ -0,0 +1,42 @@
"""add_tenant_plugin_autoupgrade_table

Revision ID: 8bcc02c9bd07
Revises: 375fe79ead14
Create Date: 2025-07-23 15:08:50.161441

"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '8bcc02c9bd07'
down_revision = '375fe79ead14'
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 ###

+ 37
- 3
api/models/account.py 查看文件

@@ -297,6 +297,40 @@ class TenantPluginPermission(Base):
)

id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID)
install_permission: Mapped[InstallPermission] = mapped_column(db.String(16), server_default="everyone")
debug_permission: Mapped[DebugPermission] = mapped_column(db.String(16), server_default="noone")
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
install_permission: Mapped[InstallPermission] = mapped_column(
db.String(16), nullable=False, server_default="everyone"
)
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())

+ 49
- 0
api/schedule/check_upgradable_plugin_task.py 查看文件

@@ -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",
)
)

+ 12
- 0
api/services/account_service.py 查看文件

@@ -29,6 +29,7 @@ from models.account import (
Tenant,
TenantAccountJoin,
TenantAccountRole,
TenantPluginAutoUpgradeStrategy,
TenantStatus,
)
from models.model import DifySetup
@@ -828,6 +829,17 @@ class TenantService:
db.session.add(tenant)
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)
db.session.commit()
return tenant

+ 87
- 0
api/services/plugin/plugin_auto_upgrade_service.py 查看文件

@@ -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

+ 166
- 0
api/tasks/process_tenant_plugin_autoupgrade_check_task.py 查看文件

@@ -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

+ 10
- 0
docker/.env.example 查看文件

@@ -1168,3 +1168,13 @@ QUEUE_MONITOR_THRESHOLD=200
QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes
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

+ 19
- 0
docker/docker-compose-template.yaml 查看文件

@@ -55,6 +55,25 @@ services:
- ssrf_proxy_network
- 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.
web:
image: langgenius/dify-web:1.6.0

+ 27
- 0
docker/docker-compose.yaml 查看文件

@@ -527,6 +527,14 @@ x-shared-env: &shared-api-worker-env
QUEUE_MONITOR_THRESHOLD: ${QUEUE_MONITOR_THRESHOLD:-200}
QUEUE_MONITOR_ALERT_EMAILS: ${QUEUE_MONITOR_ALERT_EMAILS:-}
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:
# API service
@@ -584,6 +592,25 @@ services:
- ssrf_proxy_network
- 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.
web:
image: langgenius/dify-web:1.6.0

+ 4
- 1
web/app/components/base/date-and-time-picker/common/option-list-item.tsx 查看文件

@@ -4,18 +4,21 @@ import cn from '@/utils/classnames'
type OptionListItemProps = {
isSelected: boolean
onClick: () => void
noAutoScroll?: boolean
} & React.LiHTMLAttributes<HTMLLIElement>

const OptionListItem: FC<OptionListItemProps> = ({
isSelected,
onClick,
noAutoScroll,
children,
}) => {
const listItemRef = useRef<HTMLLIElement>(null)

useEffect(() => {
if (isSelected)
if (isSelected && !noAutoScroll)
listItemRef.current?.scrollIntoView({ behavior: 'instant' })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (

+ 7
- 2
web/app/components/base/date-and-time-picker/time-picker/header.tsx 查看文件

@@ -1,13 +1,18 @@
import React from 'react'
import { useTranslation } from 'react-i18next'

const Header = () => {
type Props = {
title?: string
}
const Header = ({
title,
}: Props) => {
const { t } = useTranslation()

return (
<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'>
{t('time.title.pickTime')}
{title || t('time.title.pickTime')}
</div>
</div>
)

+ 21
- 10
web/app/components/base/date-and-time-picker/time-picker/index.tsx 查看文件

@@ -20,6 +20,9 @@ const TimePicker = ({
onChange,
onClear,
renderTrigger,
title,
minuteFilter,
popupClassName,
}: TimePickerProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
@@ -108,6 +111,15 @@ const TimePicker = ({
const displayValue = value?.format(timeFormat) || ''
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 (
<PortalToFollowElem
open={isOpen}
@@ -115,18 +127,16 @@ const TimePicker = ({
placement='bottom-end'
>
<PortalToFollowElemTrigger>
{renderTrigger ? (renderTrigger()) : (
{renderTrigger ? (renderTrigger({
inputElem,
onClick: handleClickTrigger,
isOpen,
})) : (
<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'
onClick={handleClickTrigger}
>
<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}
/>
{inputElem}
<RiTimeLine className={cn(
'h-4 w-4 shrink-0 text-text-quaternary',
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
@@ -142,14 +152,15 @@ const TimePicker = ({
</div>
)}
</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'>
{/* Header */}
<Header />
<Header title={title} />

{/* Time Options */}
<Options
selectedTime={selectedTime}
minuteFilter={minuteFilter}
handleSelectHour={handleSelectHour}
handleSelectMinute={handleSelectMinute}
handleSelectPeriod={handleSelectPeriod}

+ 3
- 1
web/app/components/base/date-and-time-picker/time-picker/options.tsx 查看文件

@@ -5,6 +5,7 @@ import OptionListItem from '../common/option-list-item'

const Options: FC<TimeOptionsProps> = ({
selectedTime,
minuteFilter,
handleSelectHour,
handleSelectMinute,
handleSelectPeriod,
@@ -33,7 +34,7 @@ const Options: FC<TimeOptionsProps> = ({
{/* Minute */}
<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
return (
<OptionListItem
@@ -57,6 +58,7 @@ const Options: FC<TimeOptionsProps> = ({
key={period}
isSelected={isSelected}
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}
</OptionListItem>

+ 11
- 1
web/app/components/base/date-and-time-picker/types.ts 查看文件

@@ -28,6 +28,7 @@ export type DatePickerProps = {
onClear: () => void
triggerWrapClassName?: string
renderTrigger?: (props: TriggerProps) => React.ReactNode
minuteFilter?: (minutes: string[]) => string[]
popupZIndexClassname?: string
}

@@ -47,13 +48,21 @@ export type DatePickerFooterProps = {
handleConfirmDate: () => void
}

export type TriggerParams = {
isOpen: boolean
inputElem: React.ReactNode
onClick: (e: React.MouseEvent) => void
}
export type TimePickerProps = {
value: Dayjs | undefined
timezone?: string
placeholder?: string
onChange: (date: Dayjs | undefined) => void
onClear: () => void
renderTrigger?: () => React.ReactNode
renderTrigger?: (props: TriggerParams) => React.ReactNode
title?: string
minuteFilter?: (minutes: string[]) => string[]
popupClassName?: string
}

export type TimePickerFooterProps = {
@@ -81,6 +90,7 @@ export type CalendarItemProps = {

export type TimeOptionsProps = {
selectedTime: Dayjs | undefined
minuteFilter?: (minutes: string[]) => string[]
handleSelectHour: (hour: string) => void
handleSelectMinute: (minute: string) => void
handleSelectPeriod: (period: Period) => void

+ 12
- 0
web/app/components/base/date-and-time-picker/utils/dayjs.ts 查看文件

@@ -2,6 +2,7 @@ import dayjs, { type Dayjs } from 'dayjs'
import type { Day } from '../types'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import tz from '@/utils/timezone.json'

dayjs.extend(utc)
dayjs.extend(timezone)
@@ -78,3 +79,14 @@ export const getHourIn12Hour = (date: Dayjs) => {
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
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)}`
}

+ 7
- 0
web/app/components/base/icons/assets/vender/line/general/search-menu.svg 查看文件

@@ -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>

+ 4
- 0
web/app/components/base/icons/assets/vender/system/auto-update-line.svg 查看文件

@@ -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>

+ 1
- 1
web/app/components/base/icons/script.mjs 查看文件

@@ -75,7 +75,7 @@ Icon.displayName = '<%= svgName %>'
export default Icon
`.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`)

const indexingRender = template(`

+ 129
- 116
web/app/components/base/icons/src/public/tracing/AliyunIcon.json
文件差异内容过多而无法显示
查看文件


+ 9
- 5
web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx 查看文件

@@ -4,12 +4,16 @@
import * as React from 'react'
import data from './AliyunIcon.json'
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'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'AliyunIcon'


+ 115
- 69
web/app/components/base/icons/src/public/tracing/AliyunIconBig.json
文件差异内容过多而无法显示
查看文件


+ 9
- 5
web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx 查看文件

@@ -4,12 +4,16 @@
import * as React from 'react'
import data from './AliyunIconBig.json'
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'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'AliyunIconBig'


+ 9
- 5
web/app/components/base/icons/src/public/tracing/WeaveIcon.tsx 查看文件

@@ -4,12 +4,16 @@
import * as React from 'react'
import data from './WeaveIcon.json'
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'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'WeaveIcon'


+ 9
- 5
web/app/components/base/icons/src/public/tracing/WeaveIconBig.tsx 查看文件

@@ -4,12 +4,16 @@
import * as React from 'react'
import data from './WeaveIconBig.json'
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'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'WeaveIconBig'


+ 2
- 2
web/app/components/base/icons/src/public/tracing/index.ts 查看文件

@@ -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 ArizeIcon } from './ArizeIcon'
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 WeaveIconBig } from './WeaveIconBig'
export { default as WeaveIcon } from './WeaveIcon'
export { default as AliyunIconBig } from './AliyunIconBig'
export { default as AliyunIcon } from './AliyunIcon'

+ 1
- 1
web/app/components/base/icons/src/vender/features/Citations.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/ContentModeration.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/Document.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/FolderUpload.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/LoveMessage.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/MessageFast.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/Microphone01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/TextToAudio.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/VirtualAssistant.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/features/Vision.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/AlertTriangle.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ArrowNarrowLeft.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ArrowUpRight.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronDownDouble.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronRight.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/RefreshCcw01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/RefreshCw05.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/arrows/ReverseLeft.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/AiText.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/ChatBot.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/ChatBotSlim.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/CuteRobot.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/ArtificialBrain.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/BarChartSquare02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/BracketsX.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/CodeBrowser.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Container.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Database01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Database03.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/FileHeart02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/GitBranch01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/PromptEngineering.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/PuzzlePiece01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/TerminalSquare.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Variable.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/development/Webhooks.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/AlignLeft.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/Collapse.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/Colors.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/ImageIndentLeft.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/LeftIndent02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/LetterSpacing01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/editor/TypeSquare.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/education/BookOpen01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/File02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileArrow01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileCheck02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileDownload02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FilePlus01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FilePlus02.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileText.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/FileUpload.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/files/Folder.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/CoinsStacked01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/GoldCoin.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/ReceiptList.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/financeAndECommerce/Tag03.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/general/AtSign.json 查看文件

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

+ 1
- 1
web/app/components/base/icons/src/vender/line/general/Bookmark.json 查看文件

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

+ 0
- 0
web/app/components/base/icons/src/vender/line/general/Check.json 查看文件


部分文件因为文件数量过多而无法显示

正在加载...
取消
保存