sycommon-python-lib 0.2.0b17__tar.gz → 0.2.0b19__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 (129) hide show
  1. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/pyproject.toml +1 -1
  3. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/database/async_base_db_service.py +0 -6
  4. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/database/base_db_service.py +0 -6
  5. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/get_llm.py +18 -13
  6. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/struct_token.py +38 -15
  7. sycommon_python_lib-0.2.0b19/src/sycommon/llm/usage_token.py +264 -0
  8. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/notice/uvicorn_monitor.py +160 -2
  9. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_pool.py +102 -2
  10. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_heartbeat_manager.py +73 -0
  11. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_service_discovery.py +54 -0
  12. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  13. sycommon_python_lib-0.2.0b17/src/sycommon/llm/usage_token.py +0 -126
  14. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/README.md +0 -0
  15. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/setup.cfg +0 -0
  16. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/__init__.py +0 -0
  17. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/cli.py +0 -0
  18. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/console.py +0 -0
  19. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/models.py +0 -0
  20. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/project.py +0 -0
  21. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/command/utils.py +0 -0
  22. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/__init__.py +0 -0
  23. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/__init__.py +0 -0
  24. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/01_basic_agent.py +0 -0
  25. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/02_tool_agent.py +0 -0
  26. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/03_structured_output.py +0 -0
  27. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/04_memory_agent.py +0 -0
  28. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/05_streaming.py +0 -0
  29. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/06_multi_agent.py +0 -0
  30. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/07_skills_agent.py +0 -0
  31. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/08_middleware.py +0 -0
  32. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/09_interrupt.py +0 -0
  33. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/10_custom_llm.py +0 -0
  34. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/11_complex_workflow.py +0 -0
  35. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/12_batch_processing.py +0 -0
  36. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/__init__.py +0 -0
  37. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/01_basic_monitoring.py +0 -0
  38. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/02_permission_control.py +0 -0
  39. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/03_tool_skill_filter.py +0 -0
  40. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/04_caching_retry.py +0 -0
  41. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/05_sanitization.py +0 -0
  42. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/06_tracking.py +0 -0
  43. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/07_advanced.py +0 -0
  44. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/08_progressive_skills.py +0 -0
  45. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/__init__.py +0 -0
  46. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/examples/middleware/override_examples.py +0 -0
  47. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/agent/get_agent.py +0 -0
  48. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/Config.py +0 -0
  49. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/DatabaseConfig.py +0 -0
  50. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/EmbeddingConfig.py +0 -0
  51. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/LLMConfig.py +0 -0
  52. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/LangfuseConfig.py +0 -0
  53. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/MQConfig.py +0 -0
  54. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/RerankerConfig.py +0 -0
  55. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/SentryConfig.py +0 -0
  56. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/config/__init__.py +0 -0
  57. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/database/async_database_service.py +0 -0
  58. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/database/database_service.py +0 -0
  59. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/health/__init__.py +0 -0
  60. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/health/health_check.py +0 -0
  61. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/health/metrics.py +0 -0
  62. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/health/ping.py +0 -0
  63. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/__init__.py +0 -0
  64. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/embedding.py +0 -0
  65. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/llm_logger.py +0 -0
  66. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/llm_tokens.py +0 -0
  67. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/llm/sy_langfuse.py +0 -0
  68. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/__init__.py +0 -0
  69. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/async_sql_logger.py +0 -0
  70. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/kafka_log.py +0 -0
  71. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/logger_levels.py +0 -0
  72. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/logger_wrapper.py +0 -0
  73. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/logging/sql_logger.py +0 -0
  74. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/__init__.py +0 -0
  75. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/context.py +0 -0
  76. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/cors.py +0 -0
  77. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/docs.py +0 -0
  78. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/exception.py +0 -0
  79. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/middleware.py +0 -0
  80. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/monitor_memory.py +0 -0
  81. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/mq.py +0 -0
  82. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/timeout.py +0 -0
  83. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/middleware/traceid.py +0 -0
  84. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/__init__.py +0 -0
  85. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/base_http.py +0 -0
  86. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/log.py +0 -0
  87. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/mqlistener_config.py +0 -0
  88. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/mqmsg_model.py +0 -0
  89. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/mqsend_config.py +0 -0
  90. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/models/sso_user.py +0 -0
  91. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/notice/__init__.py +0 -0
  92. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_client.py +0 -0
  93. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
  94. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +0 -0
  95. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -0
  96. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -0
  97. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
  98. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -0
  99. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/sentry/__init__.py +0 -0
  100. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/sentry/sy_sentry.py +0 -0
  101. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/services.py +0 -0
  102. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/sse/__init__.py +0 -0
  103. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/sse/event.py +0 -0
  104. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/sse/sse.py +0 -0
  105. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/__init__.py +0 -0
  106. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/example.py +0 -0
  107. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/example2.py +0 -0
  108. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/feign.py +0 -0
  109. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/feign_client.py +0 -0
  110. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_client_base.py +0 -0
  111. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_config_manager.py +0 -0
  112. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_service.py +0 -0
  113. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/nacos_service_registration.py +0 -0
  114. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/synacos/param.py +0 -0
  115. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tests/test_email.py +0 -0
  116. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tests/test_mq.py +0 -0
  117. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/__init__.py +0 -0
  118. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/async_utils.py +0 -0
  119. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/docs.py +0 -0
  120. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/env.py +0 -0
  121. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/merge_headers.py +0 -0
  122. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/snowflake.py +0 -0
  123. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/syemail.py +0 -0
  124. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon/tools/timing.py +0 -0
  125. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  126. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  127. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  128. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  129. {sycommon_python_lib-0.2.0b17 → sycommon_python_lib-0.2.0b19}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.0b17
3
+ Version: 0.2.0b19
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.2.0b17"
3
+ version = "0.2.0b19"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -8,13 +8,7 @@ from sycommon.logging.kafka_log import SYLogger
8
8
  class AsyncBaseDBService(metaclass=SingletonMeta):
9
9
  """数据库操作基础服务类,封装异步会话管理功能"""
10
10
 
11
- _initialized: bool = False
12
-
13
11
  def __init__(self):
14
- if AsyncBaseDBService._initialized:
15
- return
16
- AsyncBaseDBService._initialized = True
17
-
18
12
  # 获取异步引擎 (假设 DatabaseService.engine() 返回的是 AsyncEngine)
19
13
  self.engine = AsyncDatabaseService.engine()
20
14
 
@@ -8,13 +8,7 @@ from sycommon.logging.kafka_log import SYLogger
8
8
  class BaseDBService(metaclass=SingletonMeta):
9
9
  """数据库操作基础服务类,封装会话管理功能"""
10
10
 
11
- _initialized: bool = False
12
-
13
11
  def __init__(self):
14
- if BaseDBService._initialized:
15
- return
16
- BaseDBService._initialized = True
17
-
18
12
  self.engine = DatabaseService.engine()
19
13
  self.Session = sessionmaker(bind=self.engine)
20
14
 
@@ -2,7 +2,7 @@ from sycommon.llm.llm_logger import LLMLogger
2
2
  from langchain.chat_models import init_chat_model
3
3
  from sycommon.config.LLMConfig import LLMConfig
4
4
  from sycommon.llm.sy_langfuse import LangfuseInitializer
5
- from sycommon.llm.usage_token import LLMWithAutoTokenUsage
5
+ from sycommon.llm.usage_token import LLMWithAutoTokenUsage, LLMWithTokenTracking
6
6
  from typing import Any, Union
7
7
  from langchain_core.language_models import BaseChatModel
8
8
 
@@ -16,19 +16,19 @@ def get_llm(
16
16
  max_retries: int = 3,
17
17
  wrap_structured: bool = True,
18
18
  **kwargs: Any
19
- ) -> Union[LLMWithAutoTokenUsage, BaseChatModel]:
19
+ ) -> Union[LLMWithAutoTokenUsage, LLMWithTokenTracking, BaseChatModel]:
20
20
  """
21
21
  获取LLM实例
22
22
 
23
23
  Args:
24
- model: 模型名称,默认为 Qwen2.5-72B
24
+ model: 模型名称,默认为 Qwen3.5-122B-A10B
25
25
  streaming: 是否启用流式输出
26
26
  temperature: 温度参数
27
27
  timeout: 请求超时时间(秒),默认180秒
28
28
  max_retries: 最大重试次数,默认3次
29
29
  wrap_structured: 是否包装为结构化输出实例,默认True
30
30
  - True: 返回 LLMWithAutoTokenUsage,支持 with_structured_output()
31
- - False: 返回原始 BaseChatModel,保留日志和 Langfuse 跟踪
31
+ - False: 返回 LLMWithTokenTracking,保留日志、Langfuse 跟踪和 Token 统计
32
32
  **kwargs: 其他透传参数,支持 langchain init_chat_model 所有参数
33
33
  - presence_penalty: 存在惩罚
34
34
  - extra_body: 额外请求体参数,如 {"top_k": 20, "chat_template_kwargs": {"enable_thinking": False}}
@@ -38,17 +38,20 @@ def get_llm(
38
38
  - summary_prompt: 结构化输出时的摘要提示词(仅 wrap_structured=True 时有效)
39
39
 
40
40
  Returns:
41
- LLMWithAutoTokenUsage | BaseChatModel: 根据 wrap_structured 参数返回对应类型
41
+ LLMWithAutoTokenUsage | LLMWithTokenTracking | BaseChatModel: 根据 wrap_structured 参数返回对应类型
42
42
 
43
43
  Example:
44
44
  ```python
45
- # 结构化输出(默认)
46
- llm = get_llm("Qwen2.5-72B")
45
+ # 结构化输出(默认使用原生模式)
46
+ llm = get_llm("Qwen3.5-122B-A10B")
47
47
  chain = llm.with_structured_output(MyModel)
48
+ result = await chain.ainvoke([HumanMessage(content="你好")])
49
+ print(result._token_usage_) # Token 统计
48
50
 
49
- # 普通 LLM 调用,保留日志和 Langfuse
50
- llm = get_llm("Qwen2.5-72B", wrap_structured=False)
51
+ # 普通 LLM 调用,保留日志、Langfuse 和 Token 统计
52
+ llm = get_llm("Qwen3.5-122B-A10B", wrap_structured=False)
51
53
  response = await llm.ainvoke([HumanMessage(content="你好")])
54
+ print(response._token_usage_) # Token 统计
52
55
 
53
56
  # 透传额外参数
54
57
  llm = get_llm(
@@ -63,7 +66,6 @@ def get_llm(
63
66
  ```
64
67
  """
65
68
  if not model or model == "Qwen2.5-72B":
66
- # model = "Qwen2.5-72B"
67
69
  model = "Qwen3.5-122B-A10B"
68
70
  kwargs["presence_penalty"] = 0
69
71
  kwargs["extra_body"] = {
@@ -99,11 +101,14 @@ def get_llm(
99
101
  if llm is None:
100
102
  raise Exception(f"初始化原始LLM实例失败:{model}")
101
103
 
102
- # 如果不需要结构化输出包装,直接返回原始 LLM(仍保留日志和 Langfuse 跟踪)
104
+ # 如果不需要结构化输出包装,返回带 Token 统计的包装类
103
105
  if not wrap_structured:
104
- return llm
106
+ return LLMWithTokenTracking(llm, langfuse, llmConfig)
105
107
 
106
108
  # 获取kwargs中summary_prompt参数
107
109
  summary_prompt: str = kwargs.get("summary_prompt")
108
110
 
109
- return LLMWithAutoTokenUsage(llm, langfuse, llmConfig, summary_prompt, max_retries=max_retries)
111
+ return LLMWithAutoTokenUsage(
112
+ llm, langfuse, llmConfig, summary_prompt,
113
+ max_retries=max_retries
114
+ )
@@ -1,19 +1,25 @@
1
- import tiktoken
2
- from typing import Dict, List, Optional, Any
3
- from langfuse import Langfuse, LangfuseSpan, propagate_attributes
4
- from sycommon.llm.llm_logger import LLMLogger
5
- from langchain_core.runnables import Runnable, RunnableConfig
6
- from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
7
- from sycommon.llm.llm_tokens import TokensCallbackHandler
8
- from sycommon.logging.kafka_log import SYLogger
9
- from sycommon.config.LLMConfig import LLMConfig
10
- from sycommon.tools.env import get_env_var
11
1
  from sycommon.tools.merge_headers import get_header_value
2
+ from sycommon.tools.env import get_env_var
3
+ from sycommon.config.LLMConfig import LLMConfig
4
+ from sycommon.logging.kafka_log import SYLogger
5
+ from sycommon.llm.llm_tokens import TokensCallbackHandler
6
+ from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage
7
+ from langchain_core.runnables import Runnable, RunnableConfig
8
+ from sycommon.llm.llm_logger import LLMLogger
9
+ from langfuse import Langfuse, LangfuseSpan, propagate_attributes
10
+ from typing import Dict, List, Optional, Any
11
+ import tiktoken
12
+ import warnings
13
+ warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
12
14
 
13
15
 
14
16
  class StructuredRunnableWithToken(Runnable):
15
17
  """
16
18
  统一功能 Runnable:Trace追踪 + Token统计 + 自动上下文压缩
19
+
20
+ 支持两种模式:
21
+ 1. 兼容模式(is_native_mode=False):retry_chain 期望输入 {"messages": [...]}
22
+ 2. 原生模式(is_native_mode=True):retry_chain 直接接收消息列表
17
23
  """
18
24
 
19
25
  def __init__(
@@ -26,7 +32,8 @@ class StructuredRunnableWithToken(Runnable):
26
32
  enable_compression: bool = True,
27
33
  threshold_ratio: float = 0.8,
28
34
  keep_last_n: int = 2,
29
- max_compression_attempts: int = 5
35
+ max_compression_attempts: int = 5,
36
+ is_native_mode: bool = False
30
37
  ):
31
38
  super().__init__()
32
39
  self.retry_chain = retry_chain
@@ -38,6 +45,7 @@ class StructuredRunnableWithToken(Runnable):
38
45
  self.threshold_ratio = threshold_ratio
39
46
  self.keep_last_n = keep_last_n # 保留最近N条消息不压缩
40
47
  self.max_compression_attempts = max_compression_attempts # 最大压缩尝试次数
48
+ self.is_native_mode = is_native_mode # 是否为原生模式
41
49
 
42
50
  # 初始化 Tokenizer
43
51
  try:
@@ -80,7 +88,8 @@ class StructuredRunnableWithToken(Runnable):
80
88
  max_tokens = int(self.llmConfig.maxTokens * self.threshold_ratio)
81
89
  # 计算每批消息的压缩阈值:平均分配可用 token 空间
82
90
  # 预留给 system_msgs 和 keep_recent 的 token 空间
83
- reserved_tokens = self._count_tokens(system_msgs) + self._count_tokens(conversation[-self.keep_last_n:])
91
+ reserved_tokens = self._count_tokens(
92
+ system_msgs) + self._count_tokens(conversation[-self.keep_last_n:])
84
93
  available_tokens = max_tokens - reserved_tokens
85
94
 
86
95
  current_messages = messages
@@ -162,7 +171,8 @@ class StructuredRunnableWithToken(Runnable):
162
171
 
163
172
  # 根据目标 token 计算压缩比例
164
173
  if target_tokens and original_tokens > 0:
165
- compression_ratio = max(0.3, min(0.7, target_tokens / original_tokens))
174
+ compression_ratio = max(
175
+ 0.3, min(0.7, target_tokens / original_tokens))
166
176
  ratio_text = f"{int(compression_ratio * 100)}%"
167
177
  else:
168
178
  compression_ratio = 0.5
@@ -331,7 +341,14 @@ class StructuredRunnableWithToken(Runnable):
331
341
  # 【同步模式下不建议触发压缩,因为压缩本身是异步调用 LLM】
332
342
  # 如果同步也要压缩,需要用 asyncio.run(...),这里暂时保持原逻辑直接透传
333
343
  adapted_input = self._adapt_input(input)
334
- input_data = {"messages": adapted_input}
344
+
345
+ # 根据模式选择输入格式
346
+ if self.is_native_mode:
347
+ # 原生模式:直接传递消息列表
348
+ input_data = adapted_input
349
+ else:
350
+ # 兼容模式:包装成 {"messages": [...]}
351
+ input_data = {"messages": adapted_input}
335
352
 
336
353
  if span:
337
354
  span.update_trace(input=input_data)
@@ -382,7 +399,13 @@ class StructuredRunnableWithToken(Runnable):
382
399
  # 执行压缩,替换 adapted_input
383
400
  adapted_input = await self._acompress_context(adapted_input)
384
401
 
385
- input_data = {"messages": adapted_input}
402
+ # 根据模式选择输入格式
403
+ if self.is_native_mode:
404
+ # 原生模式:直接传递消息列表
405
+ input_data = adapted_input
406
+ else:
407
+ # 兼容模式:包装成 {"messages": [...]}
408
+ input_data = {"messages": adapted_input}
386
409
 
387
410
  if span:
388
411
  span.update_trace(input=input_data)
@@ -0,0 +1,264 @@
1
+ from typing import Type, List, Optional, Callable
2
+ from langfuse import Langfuse
3
+ from langchain_core.language_models import BaseChatModel
4
+ from langchain_core.runnables import Runnable, RunnableLambda
5
+ from langchain_core.output_parsers import PydanticOutputParser
6
+ from langchain_core.messages import BaseMessage, HumanMessage
7
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8
+ from pydantic import BaseModel, ValidationError, Field
9
+ from sycommon.config.LLMConfig import LLMConfig
10
+ from sycommon.llm.struct_token import StructuredRunnableWithToken
11
+
12
+
13
+ class LLMWithTokenTracking(BaseChatModel):
14
+ """
15
+ 带Token统计的LLM包装类(非结构化输出场景)
16
+ 用于 wrap_structured=False 时的 Token 统计和 Langfuse 追踪
17
+
18
+ 设计原则:最小化重写,只重写核心的 _generate 和 _agenerate
19
+ 所有其他方法(invoke, ainvoke, batch, abatch, stream, astream)
20
+ 都会自动通过 BaseChatModel 的默认实现调用这两个核心方法
21
+ """
22
+ llm: BaseChatModel = Field(default=None)
23
+ langfuse: Optional[Langfuse] = Field(default=None, exclude=True)
24
+ llmConfig: Optional[LLMConfig] = Field(default=None, exclude=True)
25
+
26
+ def __init__(self, llm: BaseChatModel, langfuse: Langfuse = None, llmConfig: LLMConfig = None, **kwargs):
27
+ super().__init__(llm=llm, langfuse=langfuse, llmConfig=llmConfig, **kwargs)
28
+
29
+ def _generate(self, messages, stop=None, run_manager=None, **kwargs):
30
+ """
31
+ 同步生成 - 核心方法
32
+ BaseChatModel 的 invoke/batch/stream 最终都会调用此方法
33
+ """
34
+ result = self.llm._generate(
35
+ messages, stop=stop, run_manager=run_manager, **kwargs)
36
+ return self._inject_token_usage(result)
37
+
38
+ async def _agenerate(self, messages, stop=None, run_manager=None, **kwargs):
39
+ """
40
+ 异步生成 - 核心方法
41
+ BaseChatModel 的 ainvoke/abatch/astream 最终都会调用此方法
42
+ """
43
+ result = await self.llm._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs)
44
+ return self._inject_token_usage(result)
45
+
46
+ def _inject_token_usage(self, result):
47
+ """
48
+ 从 LLM 结果中提取 Token 统计并注入到响应消息中
49
+ 支持单条和多条 generations
50
+ """
51
+ try:
52
+ if hasattr(result, 'generations') and result.generations:
53
+ for gen_group in result.generations:
54
+ for generation in gen_group:
55
+ if hasattr(generation, 'message') and generation.message:
56
+ # 优先从 generation_info 获取
57
+ if hasattr(generation, 'generation_info') and generation.generation_info:
58
+ gen_info = generation.generation_info
59
+ usage = {
60
+ "input_tokens": gen_info.get('input_tokens', gen_info.get('prompt_tokens', 0)),
61
+ "output_tokens": gen_info.get('output_tokens', gen_info.get('completion_tokens', 0)),
62
+ "total_tokens": gen_info.get('total_tokens', 0)
63
+ }
64
+ generation.message._token_usage_ = usage
65
+ # 从 llm_output 获取(部分模型)
66
+ elif hasattr(result, 'llm_output') and result.llm_output:
67
+ llm_output = result.llm_output
68
+ if 'token_usage' in llm_output:
69
+ token_usage = llm_output['token_usage']
70
+ usage = {
71
+ "input_tokens": token_usage.get('prompt_tokens', token_usage.get('input_tokens', 0)),
72
+ "output_tokens": token_usage.get('completion_tokens', token_usage.get('output_tokens', 0)),
73
+ "total_tokens": token_usage.get('total_tokens', 0)
74
+ }
75
+ generation.message._token_usage_ = usage
76
+ except Exception as e:
77
+ from sycommon.logging.kafka_log import SYLogger
78
+ SYLogger.warning(f"Token 统计注入失败: {str(e)}")
79
+
80
+ return result
81
+
82
+ @property
83
+ def _llm_type(self) -> str:
84
+ return self.llm._llm_type
85
+
86
+ def __getattr__(self, name):
87
+ """代理所有其他属性到原始 llm(如 model_name, max_tokens, with_structured_output 等)"""
88
+ try:
89
+ return super().__getattribute__(name)
90
+ except AttributeError:
91
+ return getattr(self.llm, name)
92
+
93
+
94
+ class LLMWithAutoTokenUsage(BaseChatModel):
95
+ """自动为结构化调用返回token_usage的LLM包装类"""
96
+ llm: BaseChatModel = Field(default=None)
97
+ langfuse: Optional[Langfuse] = Field(default=None, exclude=True)
98
+ llmConfig: Optional[LLMConfig] = Field(default=None, exclude=True)
99
+ summary_prompt: Optional[str] = Field(default=None, exclude=True)
100
+ max_retries: int = Field(default=3, exclude=True)
101
+
102
+ def __init__(self, llm: BaseChatModel, langfuse: Langfuse, llmConfig: LLMConfig, summary_prompt: str, max_retries: int = 3, **kwargs):
103
+ super().__init__(llm=llm, langfuse=langfuse, llmConfig=llmConfig,
104
+ summary_prompt=summary_prompt, max_retries=max_retries, **kwargs)
105
+
106
+ def with_structured_output(
107
+ self,
108
+ output_model: Type[BaseModel],
109
+ max_retries: int = None,
110
+ is_extract: bool = False,
111
+ override_prompt: ChatPromptTemplate = None,
112
+ custom_processors: Optional[List[Callable[[str], str]]] = None,
113
+ custom_parser: Optional[Callable[[str], BaseModel]] = None,
114
+ use_native: bool = None # True/None: 原生模式(默认), False: 兼容模式
115
+ ) -> Runnable:
116
+ """
117
+ 返回支持自动统计Token的结构化Runnable
118
+
119
+ Args:
120
+ output_model: Pydantic 模型
121
+ max_retries: 最大重试次数
122
+ is_extract: 是否为提取模式
123
+ override_prompt: 自定义提示词模板
124
+ custom_processors: 自定义文本处理器
125
+ custom_parser: 自定义解析器
126
+ use_native: 是否使用原生结构化输出(默认True)
127
+ - True/None: 使用原生模式(推荐)
128
+ - False: 使用兼容模式(仅用于不支持原生的旧模型)
129
+ """
130
+ # 使用实例的max_retries作为默认值
131
+ if max_retries is None:
132
+ max_retries = self.max_retries
133
+
134
+ # 默认使用原生模式,仅当显式指定 False 时才使用兼容模式
135
+ if use_native is False:
136
+ return self._with_compatible_structured_output(
137
+ output_model, max_retries, is_extract, override_prompt, custom_processors, custom_parser
138
+ )
139
+ else:
140
+ return self._with_native_structured_output(output_model, max_retries)
141
+
142
+ def _with_native_structured_output(
143
+ self,
144
+ output_model: Type[BaseModel],
145
+ max_retries: int
146
+ ) -> Runnable:
147
+ """
148
+ 使用模型原生的 with_structured_output 方法
149
+ 适用于:Qwen3.5-122B-A10B 等原生支持的模型
150
+ """
151
+ # 调用原生方法
152
+ native_runnable = self.llm.with_structured_output(output_model)
153
+
154
+ # 包装为支持 Token 统计和 Langfuse 追踪的 Runnable
155
+ return StructuredRunnableWithToken(
156
+ retry_chain=native_runnable,
157
+ langfuse=self.langfuse,
158
+ llmConfig=self.llmConfig,
159
+ summary_prompt=self.summary_prompt,
160
+ model_name=self.llmConfig.model if self.llmConfig else "Qwen2.5-72B",
161
+ is_native_mode=True
162
+ )
163
+
164
+ def _with_compatible_structured_output(
165
+ self,
166
+ output_model: Type[BaseModel],
167
+ max_retries: int,
168
+ is_extract: bool,
169
+ override_prompt: ChatPromptTemplate,
170
+ custom_processors: Optional[List[Callable[[str], str]]],
171
+ custom_parser: Optional[Callable[[str], BaseModel]]
172
+ ) -> Runnable:
173
+ """
174
+ 使用兼容模式(手动解析 JSON)
175
+ 适用于:不支持原生结构化输出的模型
176
+ """
177
+ parser = PydanticOutputParser(pydantic_object=output_model)
178
+
179
+ # 提示词模板
180
+ accuracy_instructions = """
181
+ 字段值的抽取准确率(0~1之间),评分规则:
182
+ 1.0(完全准确):直接从原文提取,无需任何加工,且格式与原文完全一致
183
+ 0.9(轻微处理):数据来源明确,但需进行格式标准化或冗余信息剔除(不改变原始数值)
184
+ 0.8(有限推断):数据需通过上下文关联或简单计算得出,仍有明确依据
185
+ 0.8以下(不可靠):数据需大量推测、存在歧义或来源不明,处理方式:直接忽略该数据,设置为None
186
+ """
187
+
188
+ if is_extract:
189
+ prompt = ChatPromptTemplate.from_messages([
190
+ MessagesPlaceholder(variable_name="messages"),
191
+ HumanMessage(content=f"""
192
+ 请提取信息并遵循以下规则:
193
+ 1. 准确率要求:{accuracy_instructions.strip()}
194
+ 2. 输出格式:{parser.get_format_instructions()}
195
+ """)
196
+ ])
197
+ else:
198
+ prompt = override_prompt or ChatPromptTemplate.from_messages([
199
+ MessagesPlaceholder(variable_name="messages"),
200
+ HumanMessage(content=f"""
201
+ 输出格式:{parser.get_format_instructions()}
202
+ """)
203
+ ])
204
+
205
+ # 文本处理函数
206
+ def extract_response_content(response: BaseMessage) -> str:
207
+ try:
208
+ return response.content
209
+ except Exception as e:
210
+ raise ValueError(f"提取响应内容失败:{str(e)}") from e
211
+
212
+ def strip_code_block_markers(content: str) -> str:
213
+ try:
214
+ return content.strip("```json").strip("```").strip()
215
+ except Exception as e:
216
+ raise ValueError(f"移除代码块标记失败:{str(e)}") from e
217
+
218
+ def normalize_in_json(content: str) -> str:
219
+ try:
220
+ return content.replace("None", "null").replace("none", "null").replace("NONE", "null").replace("''", '""')
221
+ except Exception as e:
222
+ raise ValueError(f"JSON格式化失败:{str(e)}") from e
223
+
224
+ def default_parse_to_pydantic(content: str) -> BaseModel:
225
+ try:
226
+ return parser.parse(content)
227
+ except (ValidationError, ValueError) as e:
228
+ raise ValueError(f"解析结构化结果失败:{str(e)}") from e
229
+
230
+ # ========== 构建处理链 ==========
231
+ base_chain = prompt | self.llm | RunnableLambda(
232
+ extract_response_content)
233
+
234
+ # 文本处理链
235
+ process_runnables = custom_processors or [
236
+ RunnableLambda(strip_code_block_markers),
237
+ RunnableLambda(normalize_in_json)
238
+ ]
239
+ process_chain = base_chain
240
+ for runnable in process_runnables:
241
+ process_chain = process_chain | runnable
242
+
243
+ # 解析链
244
+ parse_chain = process_chain | RunnableLambda(
245
+ custom_parser or default_parse_to_pydantic)
246
+
247
+ # 重试链
248
+ retry_chain = parse_chain.with_retry(
249
+ retry_if_exception_type=(ValidationError, ValueError),
250
+ stop_after_attempt=max_retries,
251
+ wait_exponential_jitter=True,
252
+ exponential_jitter_params={
253
+ "initial": 0.1, "max": 3.0, "exp_base": 2.0, "jitter": 1.0}
254
+ )
255
+
256
+ return StructuredRunnableWithToken(retry_chain, self.langfuse, self.llmConfig, self.summary_prompt)
257
+
258
+ # ========== 实现BaseChatModel抽象方法 ==========
259
+ def _generate(self, messages, stop=None, run_manager=None, ** kwargs):
260
+ return self.llm._generate(messages, stop=stop, run_manager=run_manager, ** kwargs)
261
+
262
+ @property
263
+ def _llm_type(self) -> str:
264
+ return self.llm._llm_type
@@ -9,6 +9,46 @@ from sycommon.config.Config import Config
9
9
  from sycommon.logging.kafka_log import SYLogger
10
10
 
11
11
 
12
+ def get_webhook() -> Optional[str]:
13
+ """
14
+ 获取企业微信 WebHook 配置
15
+
16
+ 支持两种配置格式:
17
+ 1. 字符串格式(旧版):
18
+ llm:
19
+ WebHook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
20
+
21
+ 2. 对象格式(新版,支持启用开关):
22
+ llm:
23
+ WebHook:
24
+ enabled: true
25
+ url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
26
+ """
27
+ try:
28
+ config = Config().config
29
+ webhook_config = config.get('llm', {}).get('WebHook')
30
+
31
+ if webhook_config is None:
32
+ return None
33
+
34
+ # 字符串格式(旧版兼容)
35
+ if isinstance(webhook_config, str):
36
+ return webhook_config
37
+
38
+ # 对象格式(新版)
39
+ if isinstance(webhook_config, dict):
40
+ # 检查是否启用
41
+ if not webhook_config.get('enabled', True):
42
+ SYLogger.debug("企业微信 WebHook 已禁用")
43
+ return None
44
+ return webhook_config.get('url')
45
+
46
+ return None
47
+ except Exception as e:
48
+ SYLogger.warning(f"读取 WebHook 配置失败: {str(e)}")
49
+ return None
50
+
51
+
12
52
  async def send_wechat_markdown_msg(
13
53
  content: str,
14
54
  webhook: str = None
@@ -55,8 +95,7 @@ async def send_webhook(error_info: dict = None, webhook: str = None):
55
95
  config = Config().config
56
96
  service_name = config.get('Name', "未知服务")
57
97
  env = config.get('Nacos', {}).get('namespaceId', '未知环境')
58
- # 注意:这里使用了大写开头的 WebHook,请确保配置文件中键名一致
59
- webHook = config.get('llm', {}).get('WebHook')
98
+ webHook = get_webhook()
60
99
  except Exception as e:
61
100
  service_name = "未知服务"
62
101
  env = "未知环境"
@@ -186,3 +225,122 @@ def run(*args, webhook: str = None, **kwargs):
186
225
 
187
226
  # 只有确实有错误时才以状态码 1 退出
188
227
  sys.exit(1)
228
+
229
+
230
+ async def send_mq_disconnect_alert(
231
+ error_msg: str,
232
+ host: str = None,
233
+ app_name: str = None,
234
+ disconnect_count: int = 0,
235
+ reconnect_attempts: int = 0,
236
+ is_recovered: bool = False
237
+ ) -> Optional[dict]:
238
+ """
239
+ 发送 MQ 连接断开告警
240
+
241
+ :param error_msg: 错误信息
242
+ :param host: RabbitMQ 主机地址
243
+ :param app_name: 应用名称(MQ连接名)
244
+ :param disconnect_count: 累计断开次数
245
+ :param reconnect_attempts: 重连尝试次数
246
+ :param is_recovered: 是否已恢复
247
+ :return: 发送结果
248
+ """
249
+ try:
250
+ config = Config().config
251
+ service_name = config.get('Name', "未知服务")
252
+ env = config.get('Nacos', {}).get('namespaceId', '未知环境')
253
+ except Exception:
254
+ service_name = "未知服务"
255
+ env = "未知环境"
256
+
257
+ webhook = get_webhook()
258
+ if not webhook:
259
+ SYLogger.debug("未配置企业微信 WebHook,跳过 MQ 断连告警")
260
+ return None
261
+
262
+ alert_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
263
+ # 显示服务名和应用名
264
+ display_name = f"{service_name}" + (f" ({app_name})" if app_name else "")
265
+
266
+ if is_recovered:
267
+ # 恢复通知
268
+ markdown_content = f"""### {display_name} RabbitMQ 连接已恢复 ✅
269
+ > 环境: <font color="info">{env}</font>
270
+ > 时间: <font color="comment">{alert_time}</font>
271
+ > 节点: <font color="comment">{host or '未知'}</font>
272
+ > 恢复前重试次数: {reconnect_attempts}
273
+ > 本次断开次数: {disconnect_count}"""
274
+ else:
275
+ # 断开告警
276
+ markdown_content = f"""### {display_name} RabbitMQ 连接断开告警 🚨
277
+ > 环境: <font color="warning">{env}</font>
278
+ > 时间: <font color="comment">{alert_time}</font>
279
+ > 节点: <font color="comment">{host or '未知'}</font>
280
+ > 累计断开次数: <font color="danger">{disconnect_count}</font>
281
+ > 重连尝试: {reconnect_attempts} 次
282
+ > 错误信息: <font color="danger">{error_msg}</font>"""
283
+
284
+ return await send_wechat_markdown_msg(
285
+ content=markdown_content,
286
+ webhook=webhook
287
+ )
288
+
289
+
290
+ async def send_nacos_disconnect_alert(
291
+ error_msg: str,
292
+ service_name: str = None,
293
+ host: str = None,
294
+ disconnect_count: int = 0,
295
+ fail_count: int = 0,
296
+ is_recovered: bool = False
297
+ ) -> Optional[dict]:
298
+ """
299
+ 发送 Nacos 连接断开告警
300
+
301
+ :param error_msg: 错误信息
302
+ :param service_name: 服务名称
303
+ :param host: 服务地址
304
+ :param disconnect_count: 累计断开次数
305
+ :param fail_count: 连续失败次数
306
+ :param is_recovered: 是否已恢复
307
+ :return: 发送结果
308
+ """
309
+ try:
310
+ config = Config().config
311
+ display_service_name = config.get('Name', "未知服务")
312
+ env = config.get('Nacos', {}).get('namespaceId', '未知环境')
313
+ except Exception:
314
+ display_service_name = "未知服务"
315
+ env = "未知环境"
316
+
317
+ webhook = get_webhook()
318
+ if not webhook:
319
+ SYLogger.debug("未配置企业微信 WebHook,跳过 Nacos 断连告警")
320
+ return None
321
+
322
+ alert_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
323
+ # 显示服务名
324
+ display_name = service_name or display_service_name
325
+
326
+ if is_recovered:
327
+ # 恢复通知
328
+ markdown_content = f"""### {display_name} Nacos 心跳已恢复 ✅
329
+ > 环境: <font color="info">{env}</font>
330
+ > 时间: <font color="comment">{alert_time}</font>
331
+ > 服务地址: <font color="comment">{host or '未知'}</font>
332
+ > 累计断开次数: {disconnect_count}"""
333
+ else:
334
+ # 断开告警
335
+ markdown_content = f"""### {display_name} Nacos 心跳失败告警 🚨
336
+ > 环境: <font color="warning">{env}</font>
337
+ > 时间: <font color="comment">{alert_time}</font>
338
+ > 服务地址: <font color="comment">{host or '未知'}</font>
339
+ > 累计断开次数: <font color="danger">{disconnect_count}</font>
340
+ > 连续失败: {fail_count} 次
341
+ > 错误信息: <font color="danger">{error_msg}</font>"""
342
+
343
+ return await send_wechat_markdown_msg(
344
+ content=markdown_content,
345
+ webhook=webhook
346
+ )