mcp-supervisor-core 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.
@@ -0,0 +1,7 @@
1
+ """MCP Supervisor Core — 基础设施包
2
+
3
+ 提供日志、路径、错误处理、国际化等底层工具,
4
+ 被 mcp-server 和 workbench-server 共同依赖。
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 統一調試日誌模組
4
+ ================
5
+
6
+ 提供統一的調試日誌功能,確保調試輸出不會干擾 MCP 通信。
7
+ 所有調試輸出都會發送到 stderr,並且只在調試模式啟用時才輸出。
8
+
9
+ 使用方法:
10
+ ```python
11
+ from .debug import debug_log
12
+
13
+ debug_log("這是一條調試信息")
14
+ ```
15
+
16
+ 環境變數控制:
17
+ - MCP_DEBUG=true/1/yes/on: 啟用調試模式
18
+ - MCP_DEBUG=false/0/no/off: 關閉調試模式(默認)
19
+
20
+ 作者: Minidoracat
21
+ """
22
+
23
+ import os
24
+ import sys
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ # === 文件日志句柄(懒初始化,按日期自动切换) ===
30
+ _log_file = None
31
+ _log_date: str = ""
32
+
33
+
34
+ def _get_file_logger():
35
+ """获取当日的文件日志句柄,日期变化时自动切换新文件。"""
36
+ global _log_file, _log_date
37
+ today = datetime.now().strftime("%Y-%m-%d")
38
+ if _log_file is not None and _log_date == today:
39
+ return _log_file
40
+ # 关闭旧句柄
41
+ if _log_file is not None:
42
+ try:
43
+ _log_file.close()
44
+ except Exception:
45
+ pass
46
+ try:
47
+ log_dir = Path.home() / ".mcp" / "logs"
48
+ log_dir.mkdir(parents=True, exist_ok=True)
49
+ _log_file = open(log_dir / f"mcp-supervisor-{today}.log", "a", encoding="utf-8")
50
+ _log_date = today
51
+ except Exception:
52
+ _log_file = None
53
+ return _log_file
54
+
55
+
56
+ def debug_log(message: Any, prefix: str = "DEBUG") -> None:
57
+ """
58
+ 输出调试信息到 stderr + 文件日志,避免污染 stdout(MCP JSON-RPC 通信通道)
59
+
60
+ - **stderr**: 仅在 ``MCP_DEBUG=true`` 时输出,供 IDE 捕获
61
+ - **文件日志**: 始终写入 ``~/.mcp/logs/mcp-supervisor-YYYY-MM-DD.log``,
62
+ 确保即使 IDE 不捕获 stderr 也有日志可查
63
+
64
+ Args:
65
+ message: 要输出的调试信息
66
+ prefix: 调试信息的前缀标识,默认为 "DEBUG"
67
+ """
68
+ try:
69
+ if not isinstance(message, str):
70
+ message = str(message)
71
+ except Exception:
72
+ return
73
+
74
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
75
+ formatted = f"[{prefix}] {message}"
76
+
77
+ # === 写入 stderr(仅 MCP_DEBUG 开启时) ===
78
+ if os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"):
79
+ try:
80
+ print(formatted, file=sys.stderr, flush=True)
81
+ except UnicodeEncodeError:
82
+ safe = message.encode("ascii", errors="replace").decode("ascii")
83
+ print(f"[{prefix}] {safe}", file=sys.stderr, flush=True)
84
+ except Exception:
85
+ pass
86
+
87
+ # === 写入文件日志(始终写入) ===
88
+ try:
89
+ fh = _get_file_logger()
90
+ if fh is not None:
91
+ fh.write(f"{timestamp} {formatted}\n")
92
+ fh.flush()
93
+ except Exception:
94
+ pass
95
+
96
+
97
+ def i18n_debug_log(message: Any) -> None:
98
+ """國際化模組專用的調試日誌"""
99
+ debug_log(message, "I18N")
100
+
101
+
102
+ def server_debug_log(message: Any) -> None:
103
+ """伺服器模組專用的調試日誌"""
104
+ debug_log(message, "SERVER")
105
+
106
+
107
+ def web_debug_log(message: Any) -> None:
108
+ """Web UI 模組專用的調試日誌"""
109
+ debug_log(message, "WEB")
110
+
111
+
112
+ # === 日志限流器 ===
113
+ _rate_limit_last: dict[str, float] = {}
114
+
115
+
116
+ def debug_log_throttled(
117
+ message: Any,
118
+ prefix: str = "DEBUG",
119
+ *,
120
+ key: str,
121
+ interval_seconds: float = 60.0,
122
+ ) -> None:
123
+ """限流版 debug_log:同一 key 在 interval_seconds 内最多输出一次。"""
124
+ import time
125
+
126
+ now = time.monotonic()
127
+ last = _rate_limit_last.get(key, 0.0)
128
+ if now - last < interval_seconds:
129
+ return
130
+ _rate_limit_last[key] = now
131
+ debug_log(message, prefix)
132
+
133
+
134
+ def is_debug_enabled() -> bool:
135
+ """檢查是否啟用了調試模式"""
136
+ return os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")
137
+
138
+
139
+ def set_debug_mode(enabled: bool) -> None:
140
+ """設置調試模式(用於測試)"""
141
+ os.environ["MCP_DEBUG"] = "true" if enabled else "false"
@@ -0,0 +1,450 @@
1
+ """
2
+ 統一錯誤處理框架
3
+ ================
4
+
5
+ 提供統一的錯誤處理機制,包括:
6
+ - 錯誤類型分類
7
+ - 用戶友好錯誤信息
8
+ - 錯誤上下文記錄
9
+ - 解決方案建議
10
+ - 國際化支持
11
+
12
+ 注意:此模組不會影響 JSON RPC 通信,所有錯誤處理都在應用層進行。
13
+ """
14
+
15
+ import os
16
+ import time
17
+ import traceback
18
+ from enum import Enum
19
+ from typing import Any
20
+
21
+ from .debug import debug_log
22
+
23
+
24
+ class ErrorType(Enum):
25
+ """錯誤類型枚舉"""
26
+
27
+ NETWORK = "network" # 網絡相關錯誤
28
+ FILE_IO = "file_io" # 文件 I/O 錯誤
29
+ PROCESS = "process" # 進程相關錯誤
30
+ TIMEOUT = "timeout" # 超時錯誤
31
+ USER_CANCEL = "user_cancel" # 用戶取消操作
32
+ SYSTEM = "system" # 系統錯誤
33
+ PERMISSION = "permission" # 權限錯誤
34
+ VALIDATION = "validation" # 數據驗證錯誤
35
+ DEPENDENCY = "dependency" # 依賴錯誤
36
+ CONFIGURATION = "config" # 配置錯誤
37
+
38
+
39
+ class ErrorSeverity(Enum):
40
+ """錯誤嚴重程度"""
41
+
42
+ LOW = "low" # 低:不影響核心功能
43
+ MEDIUM = "medium" # 中:影響部分功能
44
+ HIGH = "high" # 高:影響核心功能
45
+ CRITICAL = "critical" # 嚴重:系統無法正常運行
46
+
47
+
48
+ class ErrorHandler:
49
+ """統一錯誤處理器"""
50
+
51
+ # 錯誤類型到用戶友好信息的映射
52
+ _ERROR_MESSAGES = {
53
+ ErrorType.NETWORK: {
54
+ "zh-TW": "網絡連接出現問題",
55
+ "zh-CN": "网络连接出现问题",
56
+ "en": "Network connection issue",
57
+ },
58
+ ErrorType.FILE_IO: {
59
+ "zh-TW": "文件讀寫出現問題",
60
+ "zh-CN": "文件读写出现问题",
61
+ "en": "File read/write issue",
62
+ },
63
+ ErrorType.PROCESS: {
64
+ "zh-TW": "進程執行出現問題",
65
+ "zh-CN": "进程执行出现问题",
66
+ "en": "Process execution issue",
67
+ },
68
+ ErrorType.TIMEOUT: {
69
+ "zh-TW": "操作超時",
70
+ "zh-CN": "操作超时",
71
+ "en": "Operation timeout",
72
+ },
73
+ ErrorType.USER_CANCEL: {
74
+ "zh-TW": "用戶取消了操作",
75
+ "zh-CN": "用户取消了操作",
76
+ "en": "User cancelled the operation",
77
+ },
78
+ ErrorType.SYSTEM: {
79
+ "zh-TW": "系統出現問題",
80
+ "zh-CN": "系统出现问题",
81
+ "en": "System issue",
82
+ },
83
+ ErrorType.PERMISSION: {
84
+ "zh-TW": "權限不足",
85
+ "zh-CN": "权限不足",
86
+ "en": "Insufficient permissions",
87
+ },
88
+ ErrorType.VALIDATION: {
89
+ "zh-TW": "數據驗證失敗",
90
+ "zh-CN": "数据验证失败",
91
+ "en": "Data validation failed",
92
+ },
93
+ ErrorType.DEPENDENCY: {
94
+ "zh-TW": "依賴組件出現問題",
95
+ "zh-CN": "依赖组件出现问题",
96
+ "en": "Dependency issue",
97
+ },
98
+ ErrorType.CONFIGURATION: {
99
+ "zh-TW": "配置出現問題",
100
+ "zh-CN": "配置出现问题",
101
+ "en": "Configuration issue",
102
+ },
103
+ }
104
+
105
+ # 錯誤解決建議
106
+ _ERROR_SOLUTIONS = {
107
+ ErrorType.NETWORK: {
108
+ "zh-TW": ["檢查網絡連接是否正常", "確認防火牆設置", "嘗試重新啟動應用程序"],
109
+ "zh-CN": ["检查网络连接是否正常", "确认防火墙设置", "尝试重新启动应用程序"],
110
+ "en": [
111
+ "Check network connection",
112
+ "Verify firewall settings",
113
+ "Try restarting the application",
114
+ ],
115
+ },
116
+ ErrorType.FILE_IO: {
117
+ "zh-TW": ["檢查文件是否存在", "確認文件權限", "檢查磁盤空間是否足夠"],
118
+ "zh-CN": ["检查文件是否存在", "确认文件权限", "检查磁盘空间是否足够"],
119
+ "en": [
120
+ "Check if file exists",
121
+ "Verify file permissions",
122
+ "Check available disk space",
123
+ ],
124
+ },
125
+ ErrorType.PROCESS: {
126
+ "zh-TW": [
127
+ "檢查進程是否正在運行",
128
+ "確認系統資源是否足夠",
129
+ "嘗試重新啟動相關服務",
130
+ ],
131
+ "zh-CN": [
132
+ "检查进程是否正在运行",
133
+ "确认系统资源是否足够",
134
+ "尝试重新启动相关服务",
135
+ ],
136
+ "en": [
137
+ "Check if process is running",
138
+ "Verify system resources",
139
+ "Try restarting related services",
140
+ ],
141
+ },
142
+ ErrorType.TIMEOUT: {
143
+ "zh-TW": ["增加超時時間設置", "檢查網絡延遲", "稍後重試操作"],
144
+ "zh-CN": ["增加超时时间设置", "检查网络延迟", "稍后重试操作"],
145
+ "en": [
146
+ "Increase timeout settings",
147
+ "Check network latency",
148
+ "Retry the operation later",
149
+ ],
150
+ },
151
+ ErrorType.PERMISSION: {
152
+ "zh-TW": ["以管理員身份運行", "檢查文件/目錄權限", "聯繫系統管理員"],
153
+ "zh-CN": ["以管理员身份运行", "检查文件/目录权限", "联系系统管理员"],
154
+ "en": [
155
+ "Run as administrator",
156
+ "Check file/directory permissions",
157
+ "Contact system administrator",
158
+ ],
159
+ },
160
+ }
161
+
162
+ @staticmethod
163
+ def get_current_language() -> str:
164
+ """獲取當前語言設置"""
165
+ try:
166
+ # 嘗試從 i18n 模組獲取當前語言
167
+ from ..i18n import get_i18n_manager
168
+
169
+ return get_i18n_manager().get_current_language()
170
+ except Exception:
171
+ # 回退到環境變數或默認語言
172
+ return os.getenv("MCP_LANGUAGE", "zh-TW")
173
+
174
+ @staticmethod
175
+ def get_i18n_error_message(error_type: ErrorType) -> str:
176
+ """從國際化系統獲取錯誤信息"""
177
+ try:
178
+ from ..i18n import get_i18n_manager
179
+
180
+ i18n = get_i18n_manager()
181
+ key = f"errors.types.{error_type.value}"
182
+ message = i18n.t(key)
183
+ # 如果返回的是鍵本身,說明沒有找到翻譯,使用回退
184
+ if message == key:
185
+ raise Exception("Translation not found")
186
+ return message
187
+ except Exception:
188
+ # 回退到內建映射
189
+ language = ErrorHandler.get_current_language()
190
+ error_messages = ErrorHandler._ERROR_MESSAGES.get(error_type, {})
191
+ return error_messages.get(
192
+ language, error_messages.get("zh-TW", "發生未知錯誤")
193
+ )
194
+
195
+ @staticmethod
196
+ def get_i18n_error_solutions(error_type: ErrorType) -> list[str]:
197
+ """從國際化系統獲取錯誤解決方案"""
198
+ try:
199
+ from ..i18n import get_i18n_manager
200
+
201
+ i18n = get_i18n_manager()
202
+ key = f"errors.solutions.{error_type.value}"
203
+ i18n_result = i18n.t(key)
204
+
205
+ # 修復類型推斷問題 - 使用 Any 類型並明確檢查
206
+ from typing import Any
207
+
208
+ result: Any = i18n_result
209
+
210
+ # 檢查是否為列表類型且非空
211
+ if isinstance(result, list) and len(result) > 0:
212
+ return result
213
+
214
+ # 如果不是列表或為空,使用回退
215
+ raise Exception("Solutions not found or invalid format")
216
+ except Exception:
217
+ # 回退到內建映射
218
+ language = ErrorHandler.get_current_language()
219
+ solutions_dict = ErrorHandler._ERROR_SOLUTIONS.get(error_type, {})
220
+ return solutions_dict.get(language, solutions_dict.get("zh-TW", []))
221
+
222
+ @staticmethod
223
+ def classify_error(error: Exception) -> ErrorType:
224
+ """
225
+ 根據異常類型自動分類錯誤
226
+
227
+ Args:
228
+ error: Python 異常對象
229
+
230
+ Returns:
231
+ ErrorType: 錯誤類型
232
+ """
233
+ error_name = type(error).__name__
234
+ error_message = str(error).lower()
235
+
236
+ # 超時錯誤(優先檢查,避免被網絡錯誤覆蓋)
237
+ if "timeout" in error_name.lower() or "timeout" in error_message:
238
+ return ErrorType.TIMEOUT
239
+
240
+ # 權限錯誤(優先檢查,避免被文件錯誤覆蓋)
241
+ if "permission" in error_name.lower():
242
+ return ErrorType.PERMISSION
243
+ if any(
244
+ keyword in error_message
245
+ for keyword in ["permission denied", "access denied", "forbidden"]
246
+ ):
247
+ return ErrorType.PERMISSION
248
+
249
+ # 網絡相關錯誤
250
+ if any(
251
+ keyword in error_name.lower()
252
+ for keyword in ["connection", "network", "socket"]
253
+ ):
254
+ return ErrorType.NETWORK
255
+ if any(
256
+ keyword in error_message for keyword in ["connection", "network", "socket"]
257
+ ):
258
+ return ErrorType.NETWORK
259
+
260
+ # 文件 I/O 錯誤
261
+ if any(
262
+ keyword in error_name.lower() for keyword in ["file", "ioerror"]
263
+ ): # 使用更精確的匹配
264
+ return ErrorType.FILE_IO
265
+ if any(
266
+ keyword in error_message
267
+ for keyword in ["file", "directory", "no such file"]
268
+ ):
269
+ return ErrorType.FILE_IO
270
+
271
+ # 進程相關錯誤
272
+ if any(keyword in error_name.lower() for keyword in ["process", "subprocess"]):
273
+ return ErrorType.PROCESS
274
+ if any(
275
+ keyword in error_message for keyword in ["process", "command", "executable"]
276
+ ):
277
+ return ErrorType.PROCESS
278
+
279
+ # 驗證錯誤(僅按類名匹配 validation,避免 ValueError/TypeError 因含 value/type 被誤判)
280
+ if "validation" in error_name.lower():
281
+ return ErrorType.VALIDATION
282
+ # asyncio 流讀取單行超限等,消息類似 separator/chunk/limit
283
+ if "separator" in error_message and "chunk" in error_message:
284
+ return ErrorType.SYSTEM
285
+
286
+ # 配置錯誤
287
+ if any(
288
+ keyword in error_message for keyword in ["config", "setting", "environment"]
289
+ ):
290
+ return ErrorType.CONFIGURATION
291
+
292
+ # 默認為系統錯誤
293
+ return ErrorType.SYSTEM
294
+
295
+ @staticmethod
296
+ def format_user_error(
297
+ error: Exception,
298
+ error_type: ErrorType | None = None,
299
+ context: dict[str, Any] | None = None,
300
+ include_technical: bool = False,
301
+ ) -> str:
302
+ """
303
+ 將技術錯誤轉換為用戶友好的錯誤信息
304
+
305
+ Args:
306
+ error: Python 異常對象
307
+ error_type: 錯誤類型(可選,會自動分類)
308
+ context: 錯誤上下文信息
309
+ include_technical: 是否包含技術細節
310
+
311
+ Returns:
312
+ str: 用戶友好的錯誤信息
313
+ """
314
+ # 自動分類錯誤類型
315
+ if error_type is None:
316
+ error_type = ErrorHandler.classify_error(error)
317
+
318
+ # 獲取當前語言
319
+ language = ErrorHandler.get_current_language()
320
+
321
+ # 獲取用戶友好的錯誤信息(優先使用國際化系統)
322
+ user_message = ErrorHandler.get_i18n_error_message(error_type)
323
+
324
+ # 構建完整的錯誤信息
325
+ parts = [f"❌ {user_message}"]
326
+
327
+ # 添加上下文信息
328
+ if context:
329
+ if context.get("operation"):
330
+ if language == "en":
331
+ parts.append(f"Operation: {context['operation']}")
332
+ else:
333
+ parts.append(f"操作:{context['operation']}")
334
+
335
+ if context.get("file_path"):
336
+ if language == "en":
337
+ parts.append(f"File: {context['file_path']}")
338
+ else:
339
+ parts.append(f"文件:{context['file_path']}")
340
+
341
+ # 添加技術細節(如果需要)
342
+ if include_technical:
343
+ if language == "en":
344
+ parts.append(f"Technical details: {type(error).__name__}: {error!s}")
345
+ else:
346
+ parts.append(f"技術細節:{type(error).__name__}: {error!s}")
347
+
348
+ return "\n".join(parts)
349
+
350
+ @staticmethod
351
+ def get_error_solutions(error_type: ErrorType) -> list[str]:
352
+ """
353
+ 獲取錯誤解決建議
354
+
355
+ Args:
356
+ error_type: 錯誤類型
357
+
358
+ Returns:
359
+ List[str]: 解決建議列表
360
+ """
361
+ return ErrorHandler.get_i18n_error_solutions(error_type)
362
+
363
+ @staticmethod
364
+ def log_error_with_context(
365
+ error: Exception,
366
+ context: dict[str, Any] | None = None,
367
+ error_type: ErrorType | None = None,
368
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM,
369
+ ) -> str:
370
+ """
371
+ 記錄帶上下文的錯誤信息(不影響 JSON RPC)
372
+
373
+ Args:
374
+ error: Python 異常對象
375
+ context: 錯誤上下文信息
376
+ error_type: 錯誤類型
377
+ severity: 錯誤嚴重程度
378
+
379
+ Returns:
380
+ str: 錯誤 ID,用於追蹤
381
+ """
382
+ # 生成錯誤 ID
383
+ error_id = f"ERR_{int(time.time())}_{id(error) % 10000}"
384
+
385
+ # 自動分類錯誤
386
+ if error_type is None:
387
+ error_type = ErrorHandler.classify_error(error)
388
+
389
+ # 錯誤記錄已通過 debug_log 輸出,無需額外存儲
390
+
391
+ # 記錄到調試日誌(不影響 JSON RPC)
392
+ debug_log(f"錯誤記錄 [{error_id}]: {error_type.value} - {error!s}")
393
+
394
+ if context:
395
+ debug_log(f"錯誤上下文 [{error_id}]: {context}")
396
+
397
+ # 對於嚴重錯誤,記錄完整堆棧跟蹤
398
+ if severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
399
+ debug_log(f"錯誤堆棧 [{error_id}]:\n{traceback.format_exc()}")
400
+
401
+ return error_id
402
+
403
+ @staticmethod
404
+ def create_error_response(
405
+ error: Exception,
406
+ context: dict[str, Any] | None = None,
407
+ error_type: ErrorType | None = None,
408
+ include_solutions: bool = True,
409
+ for_user: bool = True,
410
+ ) -> dict[str, Any]:
411
+ """
412
+ 創建標準化的錯誤響應
413
+
414
+ Args:
415
+ error: Python 異常對象
416
+ context: 錯誤上下文
417
+ error_type: 錯誤類型
418
+ include_solutions: 是否包含解決建議
419
+ for_user: 是否為用戶界面使用
420
+
421
+ Returns:
422
+ Dict[str, Any]: 標準化錯誤響應
423
+ """
424
+ # 自動分類錯誤
425
+ if error_type is None:
426
+ error_type = ErrorHandler.classify_error(error)
427
+
428
+ # 記錄錯誤
429
+ error_id = ErrorHandler.log_error_with_context(error, context, error_type)
430
+
431
+ # 構建響應
432
+ response = {
433
+ "success": False,
434
+ "error_id": error_id,
435
+ "error_type": error_type.value,
436
+ "message": ErrorHandler.format_user_error(
437
+ error, error_type, context, include_technical=not for_user
438
+ ),
439
+ }
440
+
441
+ # 添加解決建議
442
+ if include_solutions:
443
+ solutions = ErrorHandler.get_error_solutions(error_type)
444
+ response["solutions"] = solutions # 即使為空列表也添加
445
+
446
+ # 添加上下文(僅用於調試)
447
+ if context and not for_user:
448
+ response["context"] = context
449
+
450
+ return response