plugin_validator.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # coding=utf-8
  2. """
  3. 插件验证服务
  4. 提供插件 Schema 验证、配置校验、测试执行等功能
  5. """
  6. import time
  7. from typing import Optional, Dict, Any
  8. from django.db.models import QuerySet
  9. from plugin.models.plugin import Plugin, PluginStatus
  10. from plugin.models.plugin_test import PluginTest, PluginTestStatus
  11. from plugin.models.plugin_version import PluginVersion
  12. from plugin.services.error_codes import PluginErrorCode
  13. from plugin.services.exception_handler import (
  14. PluginNotFoundException, PluginTestException, PluginTestTimeoutException,
  15. PluginSchemaException
  16. )
  17. from common.utils.logger import maxkb_logger
  18. class PluginValidator:
  19. """插件验证器"""
  20. # 默认测试超时时间(毫秒)
  21. DEFAULT_TIMEOUT_MS = 30000
  22. @staticmethod
  23. def validate_schema(schema: dict) -> tuple:
  24. """
  25. 验证插件 Schema 格式
  26. 返回 (is_valid: bool, errors: list)
  27. """
  28. errors = []
  29. if not isinstance(schema, dict):
  30. errors.append('Schema 必须是字典类型')
  31. return False, errors
  32. # 检查必要的字段
  33. required_fields = ['name', 'description', 'parameters']
  34. for field in required_fields:
  35. if field not in schema:
  36. errors.append(f'Schema 缺少必要字段: {field}')
  37. # 检查 parameters 格式
  38. if 'parameters' in schema:
  39. params = schema['parameters']
  40. if not isinstance(params, dict):
  41. errors.append('parameters 必须是字典类型')
  42. elif 'properties' not in params:
  43. errors.append('parameters 缺少 properties 定义')
  44. return len(errors) == 0, errors
  45. @staticmethod
  46. def validate_config(config: dict, schema: dict) -> tuple:
  47. """
  48. 验证插件配置是否符合 Schema
  49. 返回 (is_valid: bool, errors: list)
  50. """
  51. errors = []
  52. if not isinstance(config, dict):
  53. errors.append('配置必须是字典类型')
  54. return False, errors
  55. if not isinstance(schema, dict) or 'parameters' not in schema:
  56. return True, errors
  57. params_schema = schema.get('parameters', {})
  58. properties = params_schema.get('properties', {})
  59. required = params_schema.get('required', [])
  60. # 检查必填字段
  61. for field in required:
  62. if field not in config:
  63. errors.append(f'缺少必填配置项: {field}')
  64. # 检查字段类型(简化检查)
  65. for field, field_schema in properties.items():
  66. if field in config:
  67. expected_type = field_schema.get('type')
  68. value = config[field]
  69. if expected_type == 'string' and not isinstance(value, str):
  70. errors.append(f'配置项 {field} 应为字符串类型')
  71. elif expected_type == 'number' and not isinstance(value, (int, float)):
  72. errors.append(f'配置项 {field} 应为数字类型')
  73. elif expected_type == 'boolean' and not isinstance(value, bool):
  74. errors.append(f'配置项 {field} 应为布尔类型')
  75. return len(errors) == 0, errors
  76. @staticmethod
  77. def execute_test(plugin_id: str, test_input: dict = None,
  78. timeout_ms: int = None) -> PluginTest:
  79. """
  80. 执行插件测试
  81. """
  82. try:
  83. plugin = Plugin.objects.get(id=plugin_id)
  84. except Plugin.DoesNotExist:
  85. raise PluginNotFoundException(plugin_id)
  86. timeout_ms = timeout_ms or PluginValidator.DEFAULT_TIMEOUT_MS
  87. # 创建测试记录
  88. test_record = PluginTest.objects.create(
  89. plugin=plugin,
  90. version=plugin.version,
  91. test_type='smoke',
  92. status=PluginTestStatus.RUNNING,
  93. input_data=test_input or {}
  94. )
  95. start_time = time.time()
  96. try:
  97. # 执行测试逻辑(简化版:验证 Schema 和配置)
  98. is_schema_valid, schema_errors = PluginValidator.validate_schema(plugin.schema)
  99. if not is_schema_valid:
  100. raise PluginSchemaException(
  101. message='Schema 验证失败',
  102. detail={'errors': schema_errors}
  103. )
  104. is_config_valid, config_errors = PluginValidator.validate_config(
  105. plugin.config, plugin.schema
  106. )
  107. if not is_config_valid:
  108. raise PluginTestException(
  109. message='配置验证失败',
  110. detail={'errors': config_errors}
  111. )
  112. # 计算耗时
  113. duration_ms = int((time.time() - start_time) * 1000)
  114. # 更新测试记录
  115. test_record.status = PluginTestStatus.SUCCESS
  116. test_record.passed = True
  117. test_record.duration_ms = duration_ms
  118. test_record.output_data = {
  119. 'schema_valid': True,
  120. 'config_valid': True,
  121. 'message': '插件验证通过'
  122. }
  123. test_record.save()
  124. return test_record
  125. except (PluginSchemaException, PluginTestException) as e:
  126. duration_ms = int((time.time() - start_time) * 1000)
  127. test_record.status = PluginTestStatus.FAILED
  128. test_record.passed = False
  129. test_record.duration_ms = duration_ms
  130. test_record.error_code = e.error_code
  131. test_record.error_message = e.message
  132. test_record.output_data = {'detail': e.detail}
  133. test_record.save()
  134. return test_record
  135. except Exception as e:
  136. duration_ms = int((time.time() - start_time) * 1000)
  137. maxkb_logger.error(f"Plugin test execution error: {e}", exc_info=True)
  138. test_record.status = PluginTestStatus.ERROR
  139. test_record.passed = False
  140. test_record.duration_ms = duration_ms
  141. test_record.error_code = 'PLUGIN_999'
  142. test_record.error_message = str(e)
  143. test_record.save()
  144. return test_record
  145. @staticmethod
  146. def get_test_history(plugin_id: str, limit: int = 20) -> QuerySet:
  147. """获取插件测试历史"""
  148. return PluginTest.objects.filter(
  149. plugin_id=plugin_id
  150. ).order_by('-create_time')[:limit]
  151. @staticmethod
  152. def get_test_detail(test_id: str) -> Optional[PluginTest]:
  153. """获取测试详情"""
  154. try:
  155. return PluginTest.objects.get(id=test_id)
  156. except PluginTest.DoesNotExist:
  157. return None