cloneloop 0.1.0__py3-none-any.whl
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.
- cloneloop-0.1.0.dist-info/METADATA +392 -0
- cloneloop-0.1.0.dist-info/RECORD +118 -0
- cloneloop-0.1.0.dist-info/WHEEL +4 -0
- cloneloop-0.1.0.dist-info/entry_points.txt +5 -0
- mcp_ai_supervisor/__init__.py +52 -0
- mcp_ai_supervisor/__main__.py +551 -0
- mcp_ai_supervisor/debug.py +15 -0
- mcp_ai_supervisor/desktop_app/__init__.py +29 -0
- mcp_ai_supervisor/desktop_app/desktop_app.py +390 -0
- mcp_ai_supervisor/helpers/__init__.py +4 -0
- mcp_ai_supervisor/helpers/feedback_helpers.py +140 -0
- mcp_ai_supervisor/helpers/image_helpers.py +73 -0
- mcp_ai_supervisor/i18n.py +14 -0
- mcp_ai_supervisor/log_writer.py +16 -0
- mcp_ai_supervisor/logging/__init__.py +8 -0
- mcp_ai_supervisor/logging/log_parser.py +293 -0
- mcp_ai_supervisor/logging/log_tailer.py +367 -0
- mcp_ai_supervisor/py.typed +0 -0
- mcp_ai_supervisor/server.py +654 -0
- mcp_ai_supervisor/utils/__init__.py +28 -0
- mcp_ai_supervisor/utils/error_handler.py +10 -0
- mcp_ai_supervisor/utils/memory_monitor.py +11 -0
- mcp_ai_supervisor/utils/paths.py +7 -0
- mcp_ai_supervisor/utils/resource_manager.py +15 -0
- mcp_ai_supervisor/utils/structure_checker.py +291 -0
- mcp_ai_supervisor/web/__init__.py +26 -0
- mcp_ai_supervisor/web/constants/__init__.py +8 -0
- mcp_ai_supervisor/web/constants/message_codes.py +173 -0
- mcp_ai_supervisor/web/core/__init__.py +30 -0
- mcp_ai_supervisor/web/core/agent_bridge.py +957 -0
- mcp_ai_supervisor/web/core/auto_responder.py +332 -0
- mcp_ai_supervisor/web/core/context_collector.py +124 -0
- mcp_ai_supervisor/web/core/conversation_recorder.py +751 -0
- mcp_ai_supervisor/web/core/delegate_manager.py +1193 -0
- mcp_ai_supervisor/web/core/feedback_mediator.py +259 -0
- mcp_ai_supervisor/web/core/message_channel.py +551 -0
- mcp_ai_supervisor/web/core/response_builder.py +333 -0
- mcp_ai_supervisor/web/core/scene_detector.py +184 -0
- mcp_ai_supervisor/web/core/task_state.py +194 -0
- mcp_ai_supervisor/web/locales/en/translation.json +643 -0
- mcp_ai_supervisor/web/locales/zh-CN/translation.json +629 -0
- mcp_ai_supervisor/web/locales/zh-TW/translation.json +648 -0
- mcp_ai_supervisor/web/main.py +747 -0
- mcp_ai_supervisor/web/managers/__init__.py +8 -0
- mcp_ai_supervisor/web/managers/desktop_manager.py +228 -0
- mcp_ai_supervisor/web/managers/server_manager.py +170 -0
- mcp_ai_supervisor/web/managers/session_manager.py +515 -0
- mcp_ai_supervisor/web/managers/workbench_connector.py +186 -0
- mcp_ai_supervisor/web/models/__init__.py +13 -0
- mcp_ai_supervisor/web/models/feedback_result.py +16 -0
- mcp_ai_supervisor/web/models/feedback_session.py +938 -0
- mcp_ai_supervisor/web/models/session_cleaner.py +245 -0
- mcp_ai_supervisor/web/models/session_commands.py +213 -0
- mcp_ai_supervisor/web/models/session_resolver.py +158 -0
- mcp_ai_supervisor/web/models/session_timer.py +242 -0
- mcp_ai_supervisor/web/routes/__init__.py +12 -0
- mcp_ai_supervisor/web/routes/main_routes.py +965 -0
- mcp_ai_supervisor/web/routes/settings_routes.py +195 -0
- mcp_ai_supervisor/web/routes/ws_handlers.py +270 -0
- mcp_ai_supervisor/web/static/css/audio-management.css +545 -0
- mcp_ai_supervisor/web/static/css/notification-settings.css +152 -0
- mcp_ai_supervisor/web/static/css/prompt-management.css +566 -0
- mcp_ai_supervisor/web/static/css/session-management.css +1428 -0
- mcp_ai_supervisor/web/static/css/styles.css +2267 -0
- mcp_ai_supervisor/web/static/favicon.ico +0 -0
- mcp_ai_supervisor/web/static/icon-192.png +0 -0
- mcp_ai_supervisor/web/static/icon.svg +11 -0
- mcp_ai_supervisor/web/static/index.html +37 -0
- mcp_ai_supervisor/web/static/js/app.js +1721 -0
- mcp_ai_supervisor/web/static/js/i18n.js +376 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-manager.js +610 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-settings-ui.js +732 -0
- mcp_ai_supervisor/web/static/js/modules/connection-monitor.js +435 -0
- mcp_ai_supervisor/web/static/js/modules/constants/message-codes.js +168 -0
- mcp_ai_supervisor/web/static/js/modules/countdown-manager.js +273 -0
- mcp_ai_supervisor/web/static/js/modules/drag-drop-handler.js +677 -0
- mcp_ai_supervisor/web/static/js/modules/file-upload-manager.js +555 -0
- mcp_ai_supervisor/web/static/js/modules/image-handler.js +199 -0
- mcp_ai_supervisor/web/static/js/modules/logger.js +404 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-manager.js +360 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-settings.js +344 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-input-buttons.js +427 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-manager.js +414 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-modal.js +458 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-settings-ui.js +524 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-data-manager.js +1042 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-details-modal.js +594 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-ui-renderer.js +836 -0
- mcp_ai_supervisor/web/static/js/modules/session-manager.js +1059 -0
- mcp_ai_supervisor/web/static/js/modules/settings-manager.js +1002 -0
- mcp_ai_supervisor/web/static/js/modules/tab-manager.js +235 -0
- mcp_ai_supervisor/web/static/js/modules/textarea-height-manager.js +267 -0
- mcp_ai_supervisor/web/static/js/modules/ui-manager.js +578 -0
- mcp_ai_supervisor/web/static/js/modules/utils/dom-utils.js +392 -0
- mcp_ai_supervisor/web/static/js/modules/utils/status-utils.js +403 -0
- mcp_ai_supervisor/web/static/js/modules/utils/time-utils.js +440 -0
- mcp_ai_supervisor/web/static/js/modules/utils.js +557 -0
- mcp_ai_supervisor/web/static/js/modules/websocket-manager.js +875 -0
- mcp_ai_supervisor/web/static/js/modules/workbench-notification.js +103 -0
- mcp_ai_supervisor/web/static/js/vendor/marked.min.js +6 -0
- mcp_ai_supervisor/web/static/js/vendor/purify.min.js +3 -0
- mcp_ai_supervisor/web/templates/components/image-upload.html +43 -0
- mcp_ai_supervisor/web/templates/components/settings-card.html +58 -0
- mcp_ai_supervisor/web/templates/components/status-indicator.html +31 -0
- mcp_ai_supervisor/web/templates/components/toggle-switch.html +19 -0
- mcp_ai_supervisor/web/templates/feedback.html +1198 -0
- mcp_ai_supervisor/web/templates/index.html +378 -0
- mcp_ai_supervisor/web/utils/__init__.py +12 -0
- mcp_ai_supervisor/web/utils/browser.py +35 -0
- mcp_ai_supervisor/web/utils/compression_config.py +195 -0
- mcp_ai_supervisor/web/utils/compression_monitor.py +314 -0
- mcp_ai_supervisor/web/utils/ide_opener.py +286 -0
- mcp_ai_supervisor/web/utils/network.py +66 -0
- mcp_ai_supervisor/web/utils/port_manager.py +340 -0
- mcp_ai_supervisor/web/utils/session_cleanup_manager.py +543 -0
- mcp_ai_supervisor/web/utils/workbench_client.py +899 -0
- mcp_ai_supervisor/workbench/__init__.py +5 -0
- mcp_ai_supervisor/workbench/__main__.py +5 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
會話清理管理器
|
|
4
|
+
==============
|
|
5
|
+
|
|
6
|
+
統一管理 Web 會話的清理策略、統計和性能監控。
|
|
7
|
+
與內存監控系統深度集成,提供智能清理決策。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from ...debug import web_debug_log as debug_log
|
|
21
|
+
from ...log_writer import get_logger as _get_struct_logger
|
|
22
|
+
from ...utils.error_handler import ErrorHandler, ErrorType
|
|
23
|
+
from ..models.feedback_session import CleanupReason, SessionStatus
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..models.feedback_session import WebFeedbackSession
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class SessionStore(Protocol):
|
|
31
|
+
"""会话存储接口 — SessionCleanupManager 仅通过此接口操作会话。
|
|
32
|
+
|
|
33
|
+
Phase 0 引入,为 Phase 4(WebUIManager 拆分)解耦做准备。
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def session_items(self) -> list[tuple[str, WebFeedbackSession]]:
|
|
37
|
+
"""返回 [(session_id, session), ...] 快照"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def session_get(self, session_id: str) -> WebFeedbackSession | None:
|
|
41
|
+
"""按 session_id 获取会话"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def session_remove(self, session_id: str) -> None:
|
|
45
|
+
"""移除会话(同时清理 active_sessions 和 desktop_app_instances)"""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def session_count(self) -> int:
|
|
49
|
+
"""当前会话总数"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def current_session_id(self) -> str | None:
|
|
54
|
+
"""当前最新活跃会话的 ID(用于保护不被清理)"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def session_clear_all(self) -> int:
|
|
58
|
+
"""清空全部会话,返回清理数量"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CleanupPolicy:
|
|
64
|
+
"""清理策略配置"""
|
|
65
|
+
|
|
66
|
+
max_idle_time: int = 1800 # 最大空閒時間(秒)
|
|
67
|
+
max_session_age: int = 7200 # 最大會話年齡(秒)
|
|
68
|
+
max_sessions: int = 10 # 最大會話數量
|
|
69
|
+
cleanup_interval: int = 300 # 清理間隔(秒)
|
|
70
|
+
memory_pressure_threshold: float = 0.8 # 內存壓力閾值
|
|
71
|
+
enable_auto_cleanup: bool = True # 啟用自動清理
|
|
72
|
+
preserve_active_session: bool = True # 保護活躍會話
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class CleanupStats:
|
|
77
|
+
"""清理統計數據"""
|
|
78
|
+
|
|
79
|
+
total_cleanups: int = 0
|
|
80
|
+
expired_cleanups: int = 0
|
|
81
|
+
memory_pressure_cleanups: int = 0
|
|
82
|
+
manual_cleanups: int = 0
|
|
83
|
+
auto_cleanups: int = 0
|
|
84
|
+
total_sessions_cleaned: int = 0
|
|
85
|
+
total_cleanup_time: float = 0.0
|
|
86
|
+
average_cleanup_time: float = 0.0
|
|
87
|
+
last_cleanup_time: datetime | None = None
|
|
88
|
+
cleanup_efficiency: float = 0.0 # 清理效率(清理的會話數/總會話數)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CleanupTrigger(Enum):
|
|
92
|
+
"""清理觸發器類型"""
|
|
93
|
+
|
|
94
|
+
AUTO = "auto" # 自動清理
|
|
95
|
+
MEMORY_PRESSURE = "memory_pressure" # 內存壓力
|
|
96
|
+
MANUAL = "manual" # 手動清理
|
|
97
|
+
EXPIRED = "expired" # 過期清理
|
|
98
|
+
CAPACITY = "capacity" # 容量限制
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SessionCleanupManager:
|
|
102
|
+
"""會話清理管理器"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, web_ui_manager, policy: CleanupPolicy | None = None):
|
|
105
|
+
"""
|
|
106
|
+
初始化會話清理管理器
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
web_ui_manager: WebUIManager 實例(须实现 SessionStore 协议)
|
|
110
|
+
policy: 清理策略配置
|
|
111
|
+
"""
|
|
112
|
+
self.web_ui_manager = web_ui_manager
|
|
113
|
+
self._store: SessionStore = web_ui_manager
|
|
114
|
+
self.policy = policy or CleanupPolicy()
|
|
115
|
+
self.stats = CleanupStats()
|
|
116
|
+
|
|
117
|
+
# 清理狀態
|
|
118
|
+
self.is_running = False
|
|
119
|
+
self.cleanup_thread: threading.Thread | None = None
|
|
120
|
+
self._stop_event = threading.Event()
|
|
121
|
+
|
|
122
|
+
# 回調函數
|
|
123
|
+
self.cleanup_callbacks: list[Callable] = []
|
|
124
|
+
self.stats_callbacks: list[Callable] = []
|
|
125
|
+
|
|
126
|
+
# 清理歷史記錄
|
|
127
|
+
self.cleanup_history: list[dict[str, Any]] = []
|
|
128
|
+
self.max_history = 100
|
|
129
|
+
|
|
130
|
+
debug_log("SessionCleanupManager 初始化完成")
|
|
131
|
+
|
|
132
|
+
def start_auto_cleanup(self) -> bool:
|
|
133
|
+
"""啟動自動清理"""
|
|
134
|
+
if not self.policy.enable_auto_cleanup:
|
|
135
|
+
debug_log("自動清理已禁用")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
if self.is_running:
|
|
139
|
+
debug_log("自動清理已在運行")
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
self.is_running = True
|
|
144
|
+
self._stop_event.clear()
|
|
145
|
+
|
|
146
|
+
self.cleanup_thread = threading.Thread(
|
|
147
|
+
target=self._auto_cleanup_loop,
|
|
148
|
+
name="SessionCleanupManager",
|
|
149
|
+
daemon=True,
|
|
150
|
+
)
|
|
151
|
+
self.cleanup_thread.start()
|
|
152
|
+
|
|
153
|
+
debug_log(f"自動清理已啟動,間隔 {self.policy.cleanup_interval} 秒")
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
self.is_running = False
|
|
158
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
159
|
+
e, context={"operation": "啟動自動清理"}, error_type=ErrorType.SYSTEM
|
|
160
|
+
)
|
|
161
|
+
debug_log(f"啟動自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def stop_auto_cleanup(self) -> bool:
|
|
165
|
+
"""停止自動清理"""
|
|
166
|
+
if not self.is_running:
|
|
167
|
+
debug_log("自動清理未在運行")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
self.is_running = False
|
|
172
|
+
self._stop_event.set()
|
|
173
|
+
|
|
174
|
+
if self.cleanup_thread and self.cleanup_thread.is_alive():
|
|
175
|
+
self.cleanup_thread.join(timeout=5)
|
|
176
|
+
|
|
177
|
+
debug_log("自動清理已停止")
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
182
|
+
e, context={"operation": "停止自動清理"}, error_type=ErrorType.SYSTEM
|
|
183
|
+
)
|
|
184
|
+
debug_log(f"停止自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _auto_cleanup_loop(self):
|
|
188
|
+
"""自動清理主循環"""
|
|
189
|
+
debug_log("自動清理循環開始")
|
|
190
|
+
|
|
191
|
+
while not self._stop_event.is_set():
|
|
192
|
+
try:
|
|
193
|
+
# 執行清理檢查
|
|
194
|
+
self._perform_auto_cleanup()
|
|
195
|
+
|
|
196
|
+
# 等待下次清理
|
|
197
|
+
if self._stop_event.wait(self.policy.cleanup_interval):
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
202
|
+
e,
|
|
203
|
+
context={"operation": "自動清理循環"},
|
|
204
|
+
error_type=ErrorType.SYSTEM,
|
|
205
|
+
)
|
|
206
|
+
debug_log(f"自動清理循環錯誤 [錯誤ID: {error_id}]: {e}")
|
|
207
|
+
|
|
208
|
+
# 發生錯誤時等待較短時間後重試
|
|
209
|
+
if self._stop_event.wait(30):
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
debug_log("自動清理循環結束")
|
|
213
|
+
|
|
214
|
+
def _perform_auto_cleanup(self):
|
|
215
|
+
"""執行自動清理"""
|
|
216
|
+
cleanup_start_time = time.time()
|
|
217
|
+
cleaned_sessions = 0
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
total = self._store.session_count()
|
|
221
|
+
_get_struct_logger("workbench").info(
|
|
222
|
+
f"total_sessions={total}",
|
|
223
|
+
"[CLEANUP_SCAN] 开始扫描过期会话",
|
|
224
|
+
)
|
|
225
|
+
if total > self.policy.max_sessions:
|
|
226
|
+
cleaned = self._cleanup_by_capacity()
|
|
227
|
+
cleaned_sessions += cleaned
|
|
228
|
+
debug_log(f"容量限制清理了 {cleaned} 個會話")
|
|
229
|
+
|
|
230
|
+
# 2. 清理過期會話
|
|
231
|
+
cleaned = self._cleanup_expired_sessions()
|
|
232
|
+
cleaned_sessions += cleaned
|
|
233
|
+
|
|
234
|
+
# 3. 清理空閒會話
|
|
235
|
+
cleaned = self._cleanup_idle_sessions()
|
|
236
|
+
cleaned_sessions += cleaned
|
|
237
|
+
|
|
238
|
+
# 4. 更新統計
|
|
239
|
+
cleanup_duration = time.time() - cleanup_start_time
|
|
240
|
+
self._update_cleanup_stats(
|
|
241
|
+
CleanupTrigger.AUTO, cleaned_sessions, cleanup_duration
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if cleaned_sessions > 0:
|
|
245
|
+
debug_log(
|
|
246
|
+
f"自動清理完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
251
|
+
e, context={"operation": "執行自動清理"}, error_type=ErrorType.SYSTEM
|
|
252
|
+
)
|
|
253
|
+
debug_log(f"執行自動清理失敗 [錯誤ID: {error_id}]: {e}")
|
|
254
|
+
|
|
255
|
+
def trigger_cleanup(self, trigger: CleanupTrigger, force: bool = False) -> int:
|
|
256
|
+
"""觸發清理操作"""
|
|
257
|
+
cleanup_start_time = time.time()
|
|
258
|
+
cleaned_sessions = 0
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
debug_log(f"觸發清理操作,觸發器: {trigger.value},強制: {force}")
|
|
262
|
+
|
|
263
|
+
if trigger == CleanupTrigger.MEMORY_PRESSURE:
|
|
264
|
+
cleaned_sessions = (
|
|
265
|
+
self.web_ui_manager.cleanup_sessions_by_memory_pressure(force) # type: ignore[attr-defined]
|
|
266
|
+
)
|
|
267
|
+
elif trigger == CleanupTrigger.EXPIRED:
|
|
268
|
+
cleaned_sessions = self._cleanup_expired_sessions()
|
|
269
|
+
elif trigger == CleanupTrigger.CAPACITY:
|
|
270
|
+
cleaned_sessions = self._cleanup_by_capacity()
|
|
271
|
+
elif trigger == CleanupTrigger.MANUAL:
|
|
272
|
+
cleaned_sessions += self._cleanup_expired_sessions()
|
|
273
|
+
if force:
|
|
274
|
+
cleaned_sessions += (
|
|
275
|
+
self.web_ui_manager.cleanup_sessions_by_memory_pressure(force) # type: ignore[attr-defined]
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
# 自動清理
|
|
279
|
+
self._perform_auto_cleanup()
|
|
280
|
+
return 0 # 統計已在 _perform_auto_cleanup 中更新
|
|
281
|
+
|
|
282
|
+
# 更新統計
|
|
283
|
+
cleanup_duration = time.time() - cleanup_start_time
|
|
284
|
+
self._update_cleanup_stats(trigger, cleaned_sessions, cleanup_duration)
|
|
285
|
+
|
|
286
|
+
debug_log(
|
|
287
|
+
f"清理操作完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒"
|
|
288
|
+
)
|
|
289
|
+
return cleaned_sessions
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
293
|
+
e,
|
|
294
|
+
context={
|
|
295
|
+
"operation": "觸發清理",
|
|
296
|
+
"trigger": trigger.value,
|
|
297
|
+
"force": force,
|
|
298
|
+
},
|
|
299
|
+
error_type=ErrorType.SYSTEM,
|
|
300
|
+
)
|
|
301
|
+
debug_log(f"觸發清理操作失敗 [錯誤ID: {error_id}]: {e}")
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
def _cleanup_by_capacity(self) -> int:
|
|
305
|
+
"""根據容量限制清理會話"""
|
|
306
|
+
count = self._store.session_count()
|
|
307
|
+
if count <= self.policy.max_sessions:
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
excess_count = count - self.policy.max_sessions
|
|
311
|
+
current_id = self._store.current_session_id
|
|
312
|
+
|
|
313
|
+
session_priorities: list[tuple[str, Any, float]] = []
|
|
314
|
+
for session_id, session in self._store.session_items():
|
|
315
|
+
if (
|
|
316
|
+
self.policy.preserve_active_session
|
|
317
|
+
and current_id
|
|
318
|
+
and session.session_id == current_id
|
|
319
|
+
):
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
priority_score = 0.0
|
|
323
|
+
|
|
324
|
+
if session.status in [
|
|
325
|
+
SessionStatus.COMPLETED,
|
|
326
|
+
SessionStatus.ERROR,
|
|
327
|
+
SessionStatus.TIMEOUT,
|
|
328
|
+
]:
|
|
329
|
+
priority_score += 100
|
|
330
|
+
elif session.status == SessionStatus.FEEDBACK_SUBMITTED:
|
|
331
|
+
priority_score += 50
|
|
332
|
+
|
|
333
|
+
priority_score += session.get_age() / 60
|
|
334
|
+
priority_score += session.get_idle_time() / 30
|
|
335
|
+
|
|
336
|
+
session_priorities.append((session_id, session, priority_score))
|
|
337
|
+
|
|
338
|
+
session_priorities.sort(key=lambda x: x[2], reverse=True)
|
|
339
|
+
cleaned_count = 0
|
|
340
|
+
|
|
341
|
+
for i in range(min(excess_count, len(session_priorities))):
|
|
342
|
+
session_id, session, _ = session_priorities[i]
|
|
343
|
+
try:
|
|
344
|
+
session._cleanup_sync_enhanced(CleanupReason.MANUAL)
|
|
345
|
+
self._store.session_remove(session_id)
|
|
346
|
+
cleaned_count += 1
|
|
347
|
+
except Exception as e:
|
|
348
|
+
debug_log(f"容量清理會話 {session_id} 失敗: {e}")
|
|
349
|
+
|
|
350
|
+
return cleaned_count
|
|
351
|
+
|
|
352
|
+
def _cleanup_expired_sessions(self) -> int:
|
|
353
|
+
"""清理過期會話"""
|
|
354
|
+
expired_ids: list[str] = []
|
|
355
|
+
|
|
356
|
+
for session_id, session in self._store.session_items():
|
|
357
|
+
if session.is_expired() or session.get_age() > self.policy.max_session_age:
|
|
358
|
+
expired_ids.append(session_id)
|
|
359
|
+
|
|
360
|
+
cleaned_count = 0
|
|
361
|
+
for session_id in expired_ids:
|
|
362
|
+
try:
|
|
363
|
+
session = self._store.session_get(session_id)
|
|
364
|
+
if session:
|
|
365
|
+
_age = session.get_age()
|
|
366
|
+
_get_struct_logger("workbench").info(
|
|
367
|
+
f"session={session_id} age_seconds={_age:.0f} reason=expired",
|
|
368
|
+
"[CLEANUP_SESSION] 清理过期会话",
|
|
369
|
+
)
|
|
370
|
+
session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
|
|
371
|
+
self._store.session_remove(session_id)
|
|
372
|
+
cleaned_count += 1
|
|
373
|
+
except Exception as e:
|
|
374
|
+
debug_log(f"清理過期會話 {session_id} 失敗: {e}")
|
|
375
|
+
|
|
376
|
+
return cleaned_count
|
|
377
|
+
|
|
378
|
+
def _cleanup_idle_sessions(self) -> int:
|
|
379
|
+
"""清理空閒會話"""
|
|
380
|
+
current_id = self._store.current_session_id
|
|
381
|
+
idle_ids: list[str] = []
|
|
382
|
+
|
|
383
|
+
for session_id, session in self._store.session_items():
|
|
384
|
+
if (
|
|
385
|
+
self.policy.preserve_active_session
|
|
386
|
+
and current_id
|
|
387
|
+
and session.session_id == current_id
|
|
388
|
+
):
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
if session.get_idle_time() > self.policy.max_idle_time:
|
|
392
|
+
idle_ids.append(session_id)
|
|
393
|
+
|
|
394
|
+
cleaned_count = 0
|
|
395
|
+
for session_id in idle_ids:
|
|
396
|
+
try:
|
|
397
|
+
session = self._store.session_get(session_id)
|
|
398
|
+
if session:
|
|
399
|
+
session._cleanup_sync_enhanced(CleanupReason.EXPIRED)
|
|
400
|
+
self._store.session_remove(session_id)
|
|
401
|
+
cleaned_count += 1
|
|
402
|
+
except Exception as e:
|
|
403
|
+
debug_log(f"清理空閒會話 {session_id} 失敗: {e}")
|
|
404
|
+
|
|
405
|
+
return cleaned_count
|
|
406
|
+
|
|
407
|
+
def _update_cleanup_stats(
|
|
408
|
+
self, trigger: CleanupTrigger, cleaned_count: int, duration: float
|
|
409
|
+
):
|
|
410
|
+
"""更新清理統計"""
|
|
411
|
+
self.stats.total_cleanups += 1
|
|
412
|
+
self.stats.total_sessions_cleaned += cleaned_count
|
|
413
|
+
self.stats.total_cleanup_time += duration
|
|
414
|
+
self.stats.last_cleanup_time = datetime.now()
|
|
415
|
+
|
|
416
|
+
# 更新平均清理時間
|
|
417
|
+
if self.stats.total_cleanups > 0:
|
|
418
|
+
self.stats.average_cleanup_time = (
|
|
419
|
+
self.stats.total_cleanup_time / self.stats.total_cleanups
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
total_sessions = self._store.session_count() + cleaned_count
|
|
423
|
+
if total_sessions > 0:
|
|
424
|
+
self.stats.cleanup_efficiency = cleaned_count / total_sessions
|
|
425
|
+
|
|
426
|
+
# 根據觸發器類型更新統計
|
|
427
|
+
if trigger == CleanupTrigger.AUTO:
|
|
428
|
+
self.stats.auto_cleanups += 1
|
|
429
|
+
elif trigger == CleanupTrigger.MEMORY_PRESSURE:
|
|
430
|
+
self.stats.memory_pressure_cleanups += 1
|
|
431
|
+
elif trigger == CleanupTrigger.EXPIRED:
|
|
432
|
+
self.stats.expired_cleanups += 1
|
|
433
|
+
elif trigger == CleanupTrigger.MANUAL:
|
|
434
|
+
self.stats.manual_cleanups += 1
|
|
435
|
+
|
|
436
|
+
# 記錄清理歷史
|
|
437
|
+
cleanup_record = {
|
|
438
|
+
"timestamp": datetime.now().isoformat(),
|
|
439
|
+
"trigger": trigger.value,
|
|
440
|
+
"cleaned_count": cleaned_count,
|
|
441
|
+
"duration": duration,
|
|
442
|
+
"total_sessions_before": total_sessions,
|
|
443
|
+
"total_sessions_after": self._store.session_count(),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
self.cleanup_history.append(cleanup_record)
|
|
447
|
+
|
|
448
|
+
# 限制歷史記錄數量
|
|
449
|
+
if len(self.cleanup_history) > self.max_history:
|
|
450
|
+
self.cleanup_history = self.cleanup_history[-self.max_history :]
|
|
451
|
+
|
|
452
|
+
# 調用統計回調
|
|
453
|
+
for callback in self.stats_callbacks:
|
|
454
|
+
try:
|
|
455
|
+
callback(self.stats, cleanup_record)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
debug_log(f"統計回調執行失敗: {e}")
|
|
458
|
+
|
|
459
|
+
def get_cleanup_statistics(self) -> dict[str, Any]:
|
|
460
|
+
"""獲取清理統計數據"""
|
|
461
|
+
stats_dict = {
|
|
462
|
+
"total_cleanups": self.stats.total_cleanups,
|
|
463
|
+
"expired_cleanups": self.stats.expired_cleanups,
|
|
464
|
+
"memory_pressure_cleanups": self.stats.memory_pressure_cleanups,
|
|
465
|
+
"manual_cleanups": self.stats.manual_cleanups,
|
|
466
|
+
"auto_cleanups": self.stats.auto_cleanups,
|
|
467
|
+
"total_sessions_cleaned": self.stats.total_sessions_cleaned,
|
|
468
|
+
"total_cleanup_time": round(self.stats.total_cleanup_time, 2),
|
|
469
|
+
"average_cleanup_time": round(self.stats.average_cleanup_time, 2),
|
|
470
|
+
"cleanup_efficiency": round(self.stats.cleanup_efficiency, 3),
|
|
471
|
+
"last_cleanup_time": self.stats.last_cleanup_time.isoformat()
|
|
472
|
+
if self.stats.last_cleanup_time
|
|
473
|
+
else None,
|
|
474
|
+
"is_auto_cleanup_running": self.is_running,
|
|
475
|
+
"current_sessions": self._store.session_count(),
|
|
476
|
+
"policy": {
|
|
477
|
+
"max_idle_time": self.policy.max_idle_time,
|
|
478
|
+
"max_session_age": self.policy.max_session_age,
|
|
479
|
+
"max_sessions": self.policy.max_sessions,
|
|
480
|
+
"cleanup_interval": self.policy.cleanup_interval,
|
|
481
|
+
"enable_auto_cleanup": self.policy.enable_auto_cleanup,
|
|
482
|
+
"preserve_active_session": self.policy.preserve_active_session,
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return stats_dict
|
|
487
|
+
|
|
488
|
+
def get_cleanup_history(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
489
|
+
"""獲取清理歷史記錄"""
|
|
490
|
+
return self.cleanup_history[-limit:] if self.cleanup_history else []
|
|
491
|
+
|
|
492
|
+
def add_cleanup_callback(self, callback: Callable):
|
|
493
|
+
"""添加清理回調函數"""
|
|
494
|
+
if callback not in self.cleanup_callbacks:
|
|
495
|
+
self.cleanup_callbacks.append(callback)
|
|
496
|
+
debug_log("添加清理回調函數")
|
|
497
|
+
|
|
498
|
+
def add_stats_callback(self, callback: Callable):
|
|
499
|
+
"""添加統計回調函數"""
|
|
500
|
+
if callback not in self.stats_callbacks:
|
|
501
|
+
self.stats_callbacks.append(callback)
|
|
502
|
+
debug_log("添加統計回調函數")
|
|
503
|
+
|
|
504
|
+
def update_policy(self, **kwargs):
|
|
505
|
+
"""更新清理策略"""
|
|
506
|
+
for key, value in kwargs.items():
|
|
507
|
+
if hasattr(self.policy, key):
|
|
508
|
+
setattr(self.policy, key, value)
|
|
509
|
+
debug_log(f"更新清理策略 {key} = {value}")
|
|
510
|
+
else:
|
|
511
|
+
debug_log(f"未知的策略參數: {key}")
|
|
512
|
+
|
|
513
|
+
def reset_stats(self):
|
|
514
|
+
"""重置統計數據"""
|
|
515
|
+
self.stats = CleanupStats()
|
|
516
|
+
self.cleanup_history.clear()
|
|
517
|
+
debug_log("清理統計數據已重置")
|
|
518
|
+
|
|
519
|
+
def force_cleanup_all(self, exclude_current: bool = True) -> int:
|
|
520
|
+
"""強制清理所有會話"""
|
|
521
|
+
current_id = self._store.current_session_id if exclude_current else None
|
|
522
|
+
ids_to_clean: list[str] = []
|
|
523
|
+
|
|
524
|
+
for session_id, session in self._store.session_items():
|
|
525
|
+
if current_id and session.session_id == current_id:
|
|
526
|
+
continue
|
|
527
|
+
ids_to_clean.append(session_id)
|
|
528
|
+
|
|
529
|
+
cleaned_count = 0
|
|
530
|
+
for session_id in ids_to_clean:
|
|
531
|
+
try:
|
|
532
|
+
session = self._store.session_get(session_id)
|
|
533
|
+
if session:
|
|
534
|
+
session._cleanup_sync_enhanced(CleanupReason.MANUAL)
|
|
535
|
+
self._store.session_remove(session_id)
|
|
536
|
+
cleaned_count += 1
|
|
537
|
+
except Exception as e:
|
|
538
|
+
debug_log(f"強制清理會話 {session_id} 失敗: {e}")
|
|
539
|
+
|
|
540
|
+
self._update_cleanup_stats(CleanupTrigger.MANUAL, cleaned_count, 0.0)
|
|
541
|
+
|
|
542
|
+
debug_log(f"強制清理完成,清理了 {cleaned_count} 個會話")
|
|
543
|
+
return cleaned_count
|