aidev-bkplugin 2.2.4__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/PKG-INFO +2 -2
  2. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/apps.py +4 -0
  3. aidev_bkplugin-2.4.0/aidev_bkplugin/celery_runtime.py +76 -0
  4. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/settings.py +9 -0
  5. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/session.py +33 -22
  6. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/pyproject.toml +2 -2
  7. aidev_bkplugin-2.4.0/tests/test_celery_runtime.py +60 -0
  8. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_session_pv_files_views.py +94 -19
  9. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/__init__.py +0 -0
  10. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/constants.py +0 -0
  11. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/enums.py +0 -0
  12. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/management/__init__.py +0 -0
  13. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/management/commands/__init__.py +0 -0
  14. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/management/commands/upgrade_sessions.py +0 -0
  15. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/migrations/0001_initial.py +0 -0
  16. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/migrations/0002_event_delivery.py +0 -0
  17. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/migrations/__init__.py +0 -0
  18. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/models.py +0 -0
  19. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/openapi/__init__.py +0 -0
  20. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/openapi/urls.py +0 -0
  21. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/openapi/views.py +0 -0
  22. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/__init__.py +0 -0
  23. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/apigw/__init__.py +0 -0
  24. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/apigw/permissions.py +0 -0
  25. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/checkpoint/__init__.py +0 -0
  26. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/checkpoint/bk_django_saver.py +0 -0
  27. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/drf/__init__.py +0 -0
  28. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/drf/authentication.py +0 -0
  29. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/drf/decorators.py +0 -0
  30. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/drf/exception.py +0 -0
  31. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/packages/drf/renderers.py +0 -0
  32. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/permissions.py +0 -0
  33. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/private/__init__.py +0 -0
  34. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/private/permissions.py +0 -0
  35. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/private/urls.py +0 -0
  36. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/private/views.py +0 -0
  37. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/serializers/__init__.py +0 -0
  38. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/serializers/agent.py +0 -0
  39. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/serializers/chat_completion.py +0 -0
  40. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/serializers/llm.py +0 -0
  41. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/serializers/user_operation.py +0 -0
  42. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/__init__.py +0 -0
  43. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_bkplugin.py +0 -0
  44. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_builder.py +0 -0
  45. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_config.py +0 -0
  46. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_execution.py +0 -0
  47. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_helpers.py +0 -0
  48. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/agent_session.py +0 -0
  49. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/approval_resume.py +0 -0
  50. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/chat_tracing.py +0 -0
  51. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/database_event_bus.py +0 -0
  52. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/event_resource_manager.py +0 -0
  53. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/event_tracing.py +0 -0
  54. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/execution.py +0 -0
  55. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/llm.py +0 -0
  56. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/metric_runtime.py +0 -0
  57. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/otel_metrics.py +0 -0
  58. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/services/user_operation.py +0 -0
  59. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/tasks.py +0 -0
  60. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/urls.py +0 -0
  61. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/utils.py +0 -0
  62. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/__init__.py +0 -0
  63. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/agent.py +0 -0
  64. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/base.py +0 -0
  65. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/chat.py +0 -0
  66. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/chat_group.py +0 -0
  67. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/flow_agent.py +0 -0
  68. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/llm.py +0 -0
  69. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/aidev_bkplugin/views/user_operation.py +0 -0
  70. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/readme.md +0 -0
  71. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/__init__.py +0 -0
  72. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/apps.py +0 -0
  73. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/conftest.py +0 -0
  74. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/database_events/__init__.py +0 -0
  75. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/database_events/conftest.py +0 -0
  76. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/database_events/test_bus.py +0 -0
  77. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/database_events/test_resource_manager.py +0 -0
  78. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/database_events/test_tracing.py +0 -0
  79. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/packages/__init__.py +0 -0
  80. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/packages/drf/test_renderers_trace.py +0 -0
  81. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/packages/test_bk_django_saver.py +0 -0
  82. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/packages/test_bk_django_saver_async.py +0 -0
  83. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/private/__init__.py +0 -0
  84. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/private/test_private_views.py +0 -0
  85. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/__init__.py +0 -0
  86. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/agent/__init__.py +0 -0
  87. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/agent/conftest.py +0 -0
  88. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/agent/test_agent_config_fetcher.py +0 -0
  89. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/agent/test_builder.py +0 -0
  90. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/agent/test_session_manager.py +0 -0
  91. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_agent_completion.py +0 -0
  92. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_agui_handler.py +0 -0
  93. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_approval_resume.py +0 -0
  94. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_approval_resume_tracing.py +0 -0
  95. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_bkplugin.py +0 -0
  96. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_chat_tracing.py +0 -0
  97. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_event_tracing.py +0 -0
  98. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_execution.py +0 -0
  99. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_llm.py +0 -0
  100. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_otel_metrics.py +0 -0
  101. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_resume_consumer.py +0 -0
  102. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_thread_chat_completion.py +0 -0
  103. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/services/test_user_operation.py +0 -0
  104. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/settings.py +0 -0
  105. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/test_apps.py +0 -0
  106. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/test_constants.py +0 -0
  107. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/test_management_metrics.py +0 -0
  108. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/__init__.py +0 -0
  109. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_agent_view.py +0 -0
  110. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_chat_agent_code_propagation.py +0 -0
  111. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_chat_completion_serializer.py +0 -0
  112. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_chat_flow_agent.py +0 -0
  113. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_chat_stream_status.py +0 -0
  114. {aidev_bkplugin-2.2.4 → aidev_bkplugin-2.4.0}/tests/views/test_session_view.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aidev-bkplugin
3
- Version: 2.2.4
3
+ Version: 2.4.0
4
4
  Summary: AiDev Agent Bkplugin builtin functions
5
5
  Requires-Python: <3.12,>=3.11
6
- Requires-Dist: aidev-agent>=2.2.4
6
+ Requires-Dist: aidev-agent>=2.4.0
7
7
  Requires-Dist: blueapps<5.0,>=4.15.1
8
8
  Requires-Dist: django<5.0,>=4.2.28
9
9
  Provides-Extra: opentelemetry
@@ -8,6 +8,7 @@ from aidev_agent.utils.module_loading import import_string
8
8
  from django.apps import AppConfig
9
9
  from django.conf import settings
10
10
 
11
+ from aidev_bkplugin.celery_runtime import install_celery_exit_on_broker_loss
11
12
  from aidev_bkplugin.services.metric_runtime import set_metric_service
12
13
 
13
14
  try:
@@ -224,6 +225,9 @@ class AgentConfig(AppConfig):
224
225
  if custom_resource_manager:
225
226
  resource_manager.replace_defaults(import_string(custom_resource_manager)())
226
227
 
228
+ # Celery worker 断连退出,避免 consumers=0 假活。
229
+ install_celery_exit_on_broker_loss()
230
+
227
231
  # 初始化 OpenTelemetry
228
232
  init_bk_aidev_agent_otel()
229
233
 
@@ -0,0 +1,76 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Celery worker 与 broker 断连后的退出策略。
3
+
4
+ 默认 Celery 会无限重连。重连失败或 consumers 未恢复时进程仍活着,
5
+ 队列 ``consumers=0``,任务堆积,表现为服务假死。
6
+ 连上之后禁止重连,让 worker 以非 0 退出,由 PaaS 拉起新进程。
7
+ 启动阶段仍允许重试,避免 broker 尚未就绪时直接起不来。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Django settings / Celery 5 namespace 两套名字都写,兼容 blueapps 是否带 CELERY_ 前缀。
20
+ BROKER_LOSS_EXIT_SETTINGS = {
21
+ "BROKER_CONNECTION_RETRY": False,
22
+ "BROKER_CONNECTION_RETRY_ON_STARTUP": True,
23
+ "WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS": True,
24
+ "CELERY_BROKER_CONNECTION_RETRY": False,
25
+ "CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP": True,
26
+ "CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS": True,
27
+ }
28
+
29
+
30
+ def is_celery_worker(argv: list[str] | None = None) -> bool:
31
+ """判断当前进程是否为 Celery worker(不含 beat)。"""
32
+ args = list(argv if argv is not None else sys.argv)
33
+ if not args:
34
+ return False
35
+ program = Path(args[0]).name.lower()
36
+ rest = args[1:]
37
+ if program in {"celery", "celery.exe"}:
38
+ return "worker" in rest
39
+ if rest and rest[0] == "celery":
40
+ return "worker" in rest[1:]
41
+ return False
42
+
43
+
44
+ def apply_celery_exit_on_broker_loss(app: Any) -> None:
45
+ """Disable post-connect broker reconnect so a lost consumer shuts the worker down."""
46
+ conf = app.conf
47
+ conf.broker_connection_retry = False
48
+ conf.broker_connection_retry_on_startup = True
49
+ conf.worker_cancel_long_running_tasks_on_connection_loss = True
50
+
51
+
52
+ def install_celery_exit_on_broker_loss(argv: list[str] | None = None) -> bool:
53
+ """Worker 进程里关掉连上之后的 broker 重连。非 worker 或未安装 Celery 时跳过。"""
54
+ if not is_celery_worker(argv):
55
+ return False
56
+ try:
57
+ from celery import current_app
58
+ except ImportError:
59
+ logger.warning("[aidev_bkplugin] celery is not installed; skip broker-loss exit hook")
60
+ return False
61
+
62
+ apps = [current_app]
63
+ try:
64
+ from blueapps.core.celery import app as blueapps_app
65
+ except ImportError:
66
+ blueapps_app = None
67
+ if blueapps_app is not None and blueapps_app is not current_app:
68
+ apps.append(blueapps_app)
69
+
70
+ for app in apps:
71
+ apply_celery_exit_on_broker_loss(app)
72
+ logger.warning(
73
+ "[aidev_bkplugin] Celery broker reconnect after connect is disabled; "
74
+ "worker will exit on connection loss so the platform can restart it"
75
+ )
76
+ return True
@@ -37,6 +37,15 @@ RUN_VER = "ieod" if os.environ.get("BKPAAS_ENGINE_REGION", "default") == "ieod"
37
37
  # OAuth 认证配置
38
38
  BKAUTH_BACKEND_TYPE = "bk_ticket" if RUN_VER == "ieod" else "bk_token"
39
39
 
40
+ # Celery worker 连上 broker 后禁止假活重连;断连以非 0 退出,由 PaaS 拉起新 worker。
41
+ # 启动阶段仍允许重试。Django 无 namespace 与 CELERY_ 前缀两套都写,兼容 blueapps。
42
+ BROKER_CONNECTION_RETRY = False
43
+ BROKER_CONNECTION_RETRY_ON_STARTUP = True
44
+ WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True
45
+ CELERY_BROKER_CONNECTION_RETRY = False
46
+ CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
47
+ CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True
48
+
40
49
  # 仅社区版需要配置,其它版本已由开发框架处理
41
50
  if BKAUTH_BACKEND_TYPE == 'bk_token':
42
51
  OAUTH_COOKIES_PARAMS = {"bk_token": "bk_token", "bk_uid": "bk_uid"}
@@ -11,9 +11,11 @@ from aidev_agent.services.sandbox_pv_files import (
11
11
  SandboxFileInvalidRequestError,
12
12
  SandboxFileNotFoundError,
13
13
  SandboxPvFileService,
14
+ SandboxVolumeNotFoundError,
14
15
  fill_user_image_urls,
15
16
  iter_user_images_missing_url,
16
17
  validate_session_upload_files,
18
+ validate_session_upload_stats,
17
19
  )
18
20
  from bkapi_client_core.exceptions import HTTPResponseError
19
21
  from blueapps.core.exceptions import ClientBlueException, ResourceNotFound, ServerBlueException
@@ -236,11 +238,10 @@ class ChatSessionViewSet(PluginViewSet):
236
238
  def pv_files(self, request, pk, **kwargs):
237
239
  self._check_session_owner(request, pk, require_access=False)
238
240
  svc = self._make_pv_file_service(request)
239
- params = request.query_params
240
241
  try:
241
242
  data = svc.list_files(
242
243
  session_code=pk,
243
- path=params.get("path", ""),
244
+ path=request.query_params.get("path"),
244
245
  since=None,
245
246
  until=None,
246
247
  )
@@ -248,14 +249,29 @@ class ChatSessionViewSet(PluginViewSet):
248
249
  self._raise_pv_exc(exc)
249
250
  return Response(data=data)
250
251
 
252
+ @pv_files.mapping.delete
253
+ def delete_pv_file(self, request, pk, **kwargs):
254
+ self._check_session_owner(request, pk, require_access=True)
255
+ try:
256
+ self._make_pv_file_service(request).delete_file(
257
+ session_code=pk, path=request.query_params.get("path")
258
+ )
259
+ # 幂等只覆盖「文件不存在」;会话根本没有 PV 是真错误,不能当删除成功返回 200
260
+ except SandboxVolumeNotFoundError as exc:
261
+ self._raise_pv_exc(exc)
262
+ except SandboxFileNotFoundError:
263
+ pass
264
+ except SandboxFileError as exc:
265
+ self._raise_pv_exc(exc)
266
+ return Response()
267
+
251
268
  @action(["GET"], url_path="pv_files/stat", detail=True)
252
269
  def pv_files_stat(self, request, pk, **kwargs):
253
270
  self._check_session_owner(request, pk, require_access=False)
254
- path = request.query_params.get("path", "")
255
- if not path:
256
- raise ClientBlueException(message="path is required")
257
271
  try:
258
- data = self._make_pv_file_service(request).stat_file(session_code=pk, path=path)
272
+ data = self._make_pv_file_service(request).stat_file(
273
+ session_code=pk, path=request.query_params.get("path")
274
+ )
259
275
  except SandboxFileError as exc:
260
276
  self._raise_pv_exc(exc)
261
277
  return Response(data=data)
@@ -263,13 +279,10 @@ class ChatSessionViewSet(PluginViewSet):
263
279
  @action(["GET"], url_path="pv_files/preview", detail=True)
264
280
  def pv_files_preview(self, request, pk, **kwargs):
265
281
  self._check_session_owner(request, pk, require_access=False)
266
- path = request.query_params.get("path", "")
267
- if not path:
268
- raise ClientBlueException(message="path is required")
269
282
  max_bytes = _parse_positive_int(request.query_params.get("max_bytes"), 65536)
270
283
  try:
271
284
  content, truncated = self._make_pv_file_service(request).preview_file(
272
- session_code=pk, path=path, max_bytes=max_bytes
285
+ session_code=pk, path=request.query_params.get("path"), max_bytes=max_bytes
273
286
  )
274
287
  except SandboxFileError as exc:
275
288
  self._raise_pv_exc(exc)
@@ -280,13 +293,10 @@ class ChatSessionViewSet(PluginViewSet):
280
293
  @action(["GET"], url_path="pv_files/download_url", detail=True)
281
294
  def pv_files_download_url(self, request, pk, **kwargs):
282
295
  self._check_session_owner(request, pk, require_access=False)
283
- path = request.query_params.get("path", "")
284
- if not path:
285
- raise ClientBlueException(message="path is required")
286
296
  expires_in = _parse_positive_int(request.query_params.get("expires_in"), 600)
287
297
  try:
288
298
  data = self._make_pv_file_service(request).get_download_url(
289
- session_code=pk, path=path, expires_in=expires_in
299
+ session_code=pk, path=request.query_params.get("path"), expires_in=expires_in
290
300
  )
291
301
  except SandboxFileError as exc:
292
302
  self._raise_pv_exc(exc)
@@ -302,15 +312,16 @@ class ChatSessionViewSet(PluginViewSet):
302
312
  """批量上传文件到会话 PV。"""
303
313
  self._check_session_owner(request, pk, require_access=True)
304
314
  uploaded_files = request.FILES.getlist("files")
305
- files = [
306
- {
307
- "name": upload_file.name,
308
- "content": upload_file.read(),
309
- "mime_type": upload_file.content_type or "application/octet-stream",
310
- }
311
- for upload_file in uploaded_files
312
- ]
313
315
  try:
316
+ validate_session_upload_stats(uploaded_files)
317
+ files = [
318
+ {
319
+ "name": upload_file.name,
320
+ "content": upload_file.read(),
321
+ "mime_type": upload_file.content_type or "application/octet-stream",
322
+ }
323
+ for upload_file in uploaded_files
324
+ ]
314
325
  validate_session_upload_files(files)
315
326
  except SandboxFileError as exc:
316
327
  self._raise_pv_exc(exc)
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "aidev-bkplugin"
3
- version = "2.2.4"
3
+ version = "2.4.0"
4
4
  description = "AiDev Agent Bkplugin builtin functions"
5
5
  readme = "readme.md"
6
6
  requires-python = "<3.12,>=3.11"
7
7
  dependencies = [
8
- "aidev-agent>=2.2.4",
8
+ "aidev-agent>=2.4.0",
9
9
  "blueapps>=4.15.1, <5.0",
10
10
  "django>=4.2.28,<5.0",
11
11
  ]
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from types import ModuleType, SimpleNamespace
4
+
5
+ import pytest
6
+ from aidev_bkplugin import settings as plugin_settings
7
+ from aidev_bkplugin.celery_runtime import (
8
+ BROKER_LOSS_EXIT_SETTINGS,
9
+ apply_celery_exit_on_broker_loss,
10
+ install_celery_exit_on_broker_loss,
11
+ is_celery_worker,
12
+ )
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ ("argv", "expected"),
17
+ [
18
+ (["celery", "-A", "blueapps.core.celery", "worker", "-P", "threads"], True),
19
+ (["/usr/local/bin/celery", "-A", "test_app", "worker"], True),
20
+ (["manage.py", "celery", "worker", "-B"], True),
21
+ (["bin/manage.py", "celery", "-A", "test_app", "worker"], True),
22
+ (["celery", "-A", "blueapps.core.celery", "beat"], False),
23
+ (["manage.py", "celery", "beat", "-l", "info"], False),
24
+ (["gunicorn", "wsgi:application"], False),
25
+ (["manage.py", "runserver"], False),
26
+ ([], False),
27
+ ],
28
+ )
29
+ def test_is_celery_worker_detects_worker_only(argv, expected):
30
+ assert is_celery_worker(argv) is expected
31
+
32
+
33
+ def test_apply_celery_exit_on_broker_loss_disables_reconnect_after_connect():
34
+ app = SimpleNamespace(conf=SimpleNamespace())
35
+
36
+ apply_celery_exit_on_broker_loss(app)
37
+
38
+ assert app.conf.broker_connection_retry is False
39
+ assert app.conf.broker_connection_retry_on_startup is True
40
+ assert app.conf.worker_cancel_long_running_tasks_on_connection_loss is True
41
+
42
+
43
+ def test_install_applies_when_celery_is_available(mocker):
44
+ app = SimpleNamespace(conf=SimpleNamespace())
45
+ celery_mod = ModuleType("celery")
46
+ celery_mod.current_app = app
47
+ mocker.patch.dict("sys.modules", {"celery": celery_mod})
48
+
49
+ assert install_celery_exit_on_broker_loss(["celery", "-A", "app", "worker"]) is True
50
+ assert app.conf.broker_connection_retry is False
51
+ assert app.conf.broker_connection_retry_on_startup is True
52
+
53
+
54
+ def test_plugin_settings_disable_broker_reconnect_after_connect():
55
+ for name, value in BROKER_LOSS_EXIT_SETTINGS.items():
56
+ assert getattr(plugin_settings, name) is value
57
+
58
+
59
+ def test_install_skips_non_worker_process():
60
+ assert install_celery_exit_on_broker_loss(["gunicorn", "wsgi:application"]) is False
@@ -3,7 +3,7 @@
3
3
 
4
4
  覆盖点:
5
5
  - 构造沙箱文件 Service 时正确注入 PluginResourceManager + executor_info
6
- - 5 个 action 参数透传(GET list / stat / preview / download_url / upload)
6
+ - 6 个 action 参数透传(GET list / DELETE / stat / preview / download_url / upload)
7
7
  - 上传会话归属校验与沙箱文件异常映射
8
8
  - 沙箱文件异常 → blueapps 异常映射(404 / 400 / 500)
9
9
  - preview 返回 HttpResponse(text/plain) + X-Truncated 头透传
@@ -60,6 +60,7 @@ from aidev_agent.services.sandbox_pv_files import ( # noqa: E402
60
60
  SandboxFileInvalidRequestError,
61
61
  SandboxFileNotFoundError,
62
62
  SandboxFileServerError,
63
+ SandboxVolumeNotFoundError,
63
64
  )
64
65
  from aidev_bkplugin.views import session as session_mod # noqa: E402
65
66
 
@@ -203,6 +204,7 @@ class TestPvFilesGet:
203
204
  instance.list_files.return_value = {"count": 1, "results": [{"path": "a.txt"}]}
204
205
  response = view.pv_files(_request({"path": "sub/"}), pk="s1")
205
206
  assert response.data == {"count": 1, "results": [{"path": "a.txt"}]}
207
+ # 视图只透传原始入参,归一化由 Service 负责
206
208
  instance.list_files.assert_called_once_with(session_code="s1", path="sub/", since=None, until=None)
207
209
 
208
210
  def test_list_translates_not_found(self, view, mock_svc):
@@ -282,6 +284,53 @@ class TestCheckSessionOwner:
282
284
  fake_client.api.retrieve_chat_session.assert_not_called()
283
285
 
284
286
 
287
+ # ---------------------------------------------------------------------------
288
+ # pv_files DELETE
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ class TestPvFilesDelete:
293
+ def test_delete_forwards_path_and_returns_ok(self, view, mock_svc):
294
+ instance, _ = mock_svc
295
+ instance.delete_file.return_value = None
296
+ response = view.delete_pv_file(_request({"path": "files/a.txt"}, method="DELETE"), pk="s1")
297
+ assert response.status_code == 200
298
+ instance.delete_file.assert_called_once_with(session_code="s1", path="files/a.txt")
299
+ fake_client.api.retrieve_chat_session.assert_called_once()
300
+
301
+ def test_delete_invalid_path_is_not_idempotent(self, view, mock_svc):
302
+ """非法路径由 Service 判定;幂等分支不能把它一起吞成 200。"""
303
+ from blueapps.core.exceptions import ClientBlueException
304
+
305
+ instance, _ = mock_svc
306
+ instance.delete_file.side_effect = SandboxFileInvalidArgumentError("invalid path")
307
+ with pytest.raises(ClientBlueException) as excinfo:
308
+ view.delete_pv_file(_request({"path": "files/../secret.txt"}, method="DELETE"), pk="s1")
309
+ assert excinfo.value.STATUS_CODE == 400
310
+
311
+ def test_delete_not_found_is_idempotent(self, view, mock_svc):
312
+ instance, _ = mock_svc
313
+ instance.delete_file.side_effect = SandboxFileNotFoundError("nf")
314
+ response = view.delete_pv_file(_request({"path": "files/gone.txt"}, method="DELETE"), pk="s1")
315
+ assert response.status_code == 200
316
+
317
+ def test_delete_missing_volume_is_not_idempotent(self, view, mock_svc):
318
+ """会话没有 PV 不属于「文件已删掉」,不能被幂等吞成 200。"""
319
+ from blueapps.core.exceptions import ResourceNotFound
320
+
321
+ instance, _ = mock_svc
322
+ instance.delete_file.side_effect = SandboxVolumeNotFoundError("session s1 未初始化沙箱 PersistentVolume")
323
+ with pytest.raises(ResourceNotFound):
324
+ view.delete_pv_file(_request({"path": "files/a.txt"}, method="DELETE"), pk="s1")
325
+
326
+ def test_delete_checks_owner(self, view, mock_svc):
327
+ from blueapps.core.exceptions import ClientBlueException
328
+
329
+ fake_client.api.retrieve_chat_session.side_effect = TestCheckSessionOwner._make_http_error(403)
330
+ with pytest.raises(ClientBlueException):
331
+ view.delete_pv_file(_request({"path": "files/a.txt"}, method="DELETE"), pk="s1")
332
+
333
+
285
334
  # ---------------------------------------------------------------------------
286
335
  # pv_files_stat
287
336
  # ---------------------------------------------------------------------------
@@ -295,12 +344,6 @@ class TestPvFilesStat:
295
344
  assert response.data == {"exists": True, "size": 42}
296
345
  instance.stat_file.assert_called_once_with(session_code="s1", path="x.txt")
297
346
 
298
- def test_stat_requires_path(self, view, mock_svc):
299
- from blueapps.core.exceptions import ClientBlueException
300
-
301
- with pytest.raises(ClientBlueException):
302
- view.pv_files_stat(_request({}), pk="s1")
303
-
304
347
  def test_stat_translates_invalid_arg(self, view, mock_svc):
305
348
  from blueapps.core.exceptions import ClientBlueException
306
349
 
@@ -354,12 +397,6 @@ class TestPvFilesPreview:
354
397
  view.pv_files_preview(_request({"path": "x.bin"}), pk="s1")
355
398
  assert excinfo.value.STATUS_CODE == 400
356
399
 
357
- def test_preview_requires_path(self, view, mock_svc):
358
- from blueapps.core.exceptions import ClientBlueException
359
-
360
- with pytest.raises(ClientBlueException):
361
- view.pv_files_preview(_request({}), pk="s1")
362
-
363
400
 
364
401
  # ---------------------------------------------------------------------------
365
402
  # pv_files_download_url
@@ -389,12 +426,6 @@ class TestPvFilesDownloadUrl:
389
426
  view.pv_files_download_url(_request({"path": "x.txt"}), pk="s1")
390
427
  assert excinfo.value.STATUS_CODE == 500
391
428
 
392
- def test_download_url_requires_path(self, view, mock_svc):
393
- from blueapps.core.exceptions import ClientBlueException
394
-
395
- with pytest.raises(ClientBlueException):
396
- view.pv_files_download_url(_request({}), pk="s1")
397
-
398
429
 
399
430
  class TestPvFilesUpload:
400
431
  def test_upload_forwards_files_to_local_service(self, view, mock_svc):
@@ -432,6 +463,50 @@ class TestPvFilesUpload:
432
463
  instance.upload_files.assert_not_called()
433
464
  session_mod.PluginResourceManager.return_value.get_client.return_value.api.retrieve_latest_skill_version_image.assert_not_called()
434
465
 
466
+ def test_upload_rejects_oversized_file_without_reading(self, view, mock_svc):
467
+ from aidev_agent.services.sandbox_pv_files import MAX_SESSION_UPLOAD_FILE_SIZE
468
+ from blueapps.core.exceptions import ClientBlueException
469
+
470
+ instance, _ = mock_svc
471
+ uploaded_file = SimpleNamespace(
472
+ name="big.txt",
473
+ size=MAX_SESSION_UPLOAD_FILE_SIZE + 1,
474
+ content_type="text/plain",
475
+ read=MagicMock(side_effect=AssertionError("oversized file must not be read")),
476
+ )
477
+
478
+ with pytest.raises(ClientBlueException) as excinfo:
479
+ view.pv_files_upload(_request(method="POST", files=[uploaded_file]), pk="s1")
480
+
481
+ assert str(MAX_SESSION_UPLOAD_FILE_SIZE) in excinfo.value.message
482
+ assert excinfo.value.STATUS_CODE == 400
483
+ uploaded_file.read.assert_not_called()
484
+ instance.upload_files.assert_not_called()
485
+
486
+ def test_upload_rejects_too_many_files_without_reading(self, view, mock_svc):
487
+ from aidev_agent.services.sandbox_pv_files import MAX_SESSION_UPLOAD_FILES
488
+ from blueapps.core.exceptions import ClientBlueException
489
+
490
+ instance, _ = mock_svc
491
+ uploaded_files = [
492
+ SimpleNamespace(
493
+ name=f"f{index}.txt",
494
+ size=1,
495
+ content_type="text/plain",
496
+ read=MagicMock(side_effect=AssertionError("too many files must not be read")),
497
+ )
498
+ for index in range(MAX_SESSION_UPLOAD_FILES + 1)
499
+ ]
500
+
501
+ with pytest.raises(ClientBlueException) as excinfo:
502
+ view.pv_files_upload(_request(method="POST", files=uploaded_files), pk="s1")
503
+
504
+ assert str(MAX_SESSION_UPLOAD_FILES) in excinfo.value.message
505
+ assert excinfo.value.STATUS_CODE == 400
506
+ for uploaded_file in uploaded_files:
507
+ uploaded_file.read.assert_not_called()
508
+ instance.upload_files.assert_not_called()
509
+
435
510
 
436
511
  # ---------------------------------------------------------------------------
437
512
  # 异常映射:SandboxFileError 基类兜底为 ServerBlueException
File without changes