pdd-skills 3.0.0

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 (261) hide show
  1. package/README.md +1478 -0
  2. package/bin/pdd.js +354 -0
  3. package/config/bpmn-rules.yaml +166 -0
  4. package/config/checkstyle.xml +105 -0
  5. package/config/eslint.config.js +48 -0
  6. package/config/pmd.xml +91 -0
  7. package/config/prd-rules.yaml +113 -0
  8. package/config/ruff.toml +45 -0
  9. package/config/sqlfluff.cfg +82 -0
  10. package/hooks/hook-executor.js +332 -0
  11. package/index.js +43 -0
  12. package/lib/api-routes.js +750 -0
  13. package/lib/api-server.js +408 -0
  14. package/lib/cache/cache-config.js +209 -0
  15. package/lib/cache/system-cache.js +852 -0
  16. package/lib/config-manager.js +373 -0
  17. package/lib/generate.js +528 -0
  18. package/lib/grpc/grpc-routes.js +1134 -0
  19. package/lib/grpc/grpc-server.js +912 -0
  20. package/lib/grpc/proto-definitions.js +1033 -0
  21. package/lib/init.js +172 -0
  22. package/lib/iteration/auto-fixer.js +1025 -0
  23. package/lib/iteration/auto-reviewer.js +923 -0
  24. package/lib/iteration/controller.js +577 -0
  25. package/lib/list.js +130 -0
  26. package/lib/mcp-server.js +548 -0
  27. package/lib/openclaw/api-integration.js +535 -0
  28. package/lib/openclaw/cli-integration.js +567 -0
  29. package/lib/openclaw/data-sync.js +845 -0
  30. package/lib/openclaw/openclaw-adapter.js +783 -0
  31. package/lib/plugin/example-plugins/code-stats/index.js +332 -0
  32. package/lib/plugin/example-plugins/code-stats/plugin.json +1 -0
  33. package/lib/plugin/example-plugins/custom-linter/index.js +472 -0
  34. package/lib/plugin/example-plugins/custom-linter/plugin.json +1 -0
  35. package/lib/plugin/example-plugins/hello-world/index.js +86 -0
  36. package/lib/plugin/example-plugins/hello-world/plugin.json +1 -0
  37. package/lib/plugin/plugin-manager.js +655 -0
  38. package/lib/plugin/plugin-sdk.js +565 -0
  39. package/lib/plugin/sandbox.js +627 -0
  40. package/lib/quality/rules/maintainability.js +418 -0
  41. package/lib/quality/rules/performance.js +498 -0
  42. package/lib/quality/rules/readability.js +441 -0
  43. package/lib/quality/rules/robustness.js +504 -0
  44. package/lib/quality/rules/security.js +444 -0
  45. package/lib/quality/scorer.js +576 -0
  46. package/lib/report.js +669 -0
  47. package/lib/sdk-base.js +301 -0
  48. package/lib/sdk-js.js +446 -0
  49. package/lib/sdk-python/README.md +546 -0
  50. package/lib/sdk-python/examples/basic_usage.py +450 -0
  51. package/lib/sdk-python/pdd_sdk/__init__.py +180 -0
  52. package/lib/sdk-python/pdd_sdk/client.py +1170 -0
  53. package/lib/sdk-python/pdd_sdk/events.py +423 -0
  54. package/lib/sdk-python/pdd_sdk/exceptions.py +158 -0
  55. package/lib/sdk-python/pdd_sdk/models.py +518 -0
  56. package/lib/sdk-python/pdd_sdk/utils.py +759 -0
  57. package/lib/token/budget-alert.js +367 -0
  58. package/lib/token/budget-manager.js +485 -0
  59. package/lib/update.js +54 -0
  60. package/lib/utils/logger.js +88 -0
  61. package/lib/verify.js +741 -0
  62. package/lib/version.js +52 -0
  63. package/lib/vm/README.md +102 -0
  64. package/lib/vm/dashboard/api-routes.js +669 -0
  65. package/lib/vm/dashboard/server.js +391 -0
  66. package/lib/vm/dashboard/sse.js +358 -0
  67. package/lib/vm/dashboard/static/css/dashboard.css +1378 -0
  68. package/lib/vm/dashboard/static/index.html +118 -0
  69. package/lib/vm/dashboard/static/js/app.js +949 -0
  70. package/lib/vm/dashboard/static/js/charts.js +913 -0
  71. package/lib/vm/dashboard/static/js/kanban-view.js +1053 -0
  72. package/lib/vm/dashboard/static/js/pipeline-view.js +463 -0
  73. package/lib/vm/dashboard/static/js/quality-view.js +598 -0
  74. package/lib/vm/dashboard/static/js/system-view.js +1021 -0
  75. package/lib/vm/data-provider.js +1191 -0
  76. package/lib/vm/event-bus.js +402 -0
  77. package/lib/vm/hooks/extract-hook.js +307 -0
  78. package/lib/vm/hooks/generate-hook.js +374 -0
  79. package/lib/vm/hooks/hook-interface.js +458 -0
  80. package/lib/vm/hooks/report-hook.js +331 -0
  81. package/lib/vm/hooks/verify-hook.js +454 -0
  82. package/lib/vm/models.js +1003 -0
  83. package/lib/vm/reconciler.js +855 -0
  84. package/lib/vm/scanner.js +988 -0
  85. package/lib/vm/state-schema.js +955 -0
  86. package/lib/vm/state-store.js +733 -0
  87. package/lib/vm/tui/components/card.js +339 -0
  88. package/lib/vm/tui/components/progress-bar.js +368 -0
  89. package/lib/vm/tui/components/sparkline.js +327 -0
  90. package/lib/vm/tui/components/status-light.js +294 -0
  91. package/lib/vm/tui/components/table.js +370 -0
  92. package/lib/vm/tui/input.js +335 -0
  93. package/lib/vm/tui/renderer.js +548 -0
  94. package/lib/vm/tui/screens/kanban-screen.js +397 -0
  95. package/lib/vm/tui/screens/overview-screen.js +357 -0
  96. package/lib/vm/tui/screens/quality-screen.js +336 -0
  97. package/lib/vm/tui/screens/system-screen.js +379 -0
  98. package/lib/vm/tui/tui.js +805 -0
  99. package/package.json +1 -0
  100. package/scripts/cso-analyzer.js +198 -0
  101. package/scripts/eval-runner.js +359 -0
  102. package/scripts/i18n-checker.js +109 -0
  103. package/scripts/linter/activiti-linter.js +272 -0
  104. package/scripts/linter/prd-linter.js +162 -0
  105. package/scripts/linter/report-generator.js +207 -0
  106. package/scripts/linter/run-linters.js +285 -0
  107. package/scripts/linter/sql-linter.js +166 -0
  108. package/scripts/token-analyzer.js +162 -0
  109. package/scripts/vm-test.js +180 -0
  110. package/skills/core/official-doc-writer/LICENSE +21 -0
  111. package/skills/core/official-doc-writer/README.md +232 -0
  112. package/skills/core/official-doc-writer/SKILL.md +475 -0
  113. package/skills/core/official-doc-writer/_meta.json +1 -0
  114. package/skills/core/official-doc-writer/document_generator.py +580 -0
  115. package/skills/core/official-doc-writer/evals/default-evals.json +1 -0
  116. package/skills/core/official-doc-writer/examples.md +150 -0
  117. package/skills/core/official-doc-writer/fonts/FONTS_LIST.md +45 -0
  118. package/skills/core/official-doc-writer/fonts/README.md +141 -0
  119. package/skills/core/official-doc-writer/fonts/SIMFANG.TTF +0 -0
  120. package/skills/core/official-doc-writer/fonts/SIMHEI.TTF +0 -0
  121. package/skills/core/official-doc-writer/fonts/SIMKAI.TTF +0 -0
  122. package/skills/core/official-doc-writer/fonts/SIMSUN.TTC +0 -0
  123. package/skills/core/official-doc-writer/fonts//346/226/271/346/255/243/345/260/217/346/240/207/345/256/213GBK.TTF +0 -0
  124. package/skills/core/official-doc-writer/references/GBT_9704-2012_/345/205/232/346/224/277/346/234/272/345/205/263/345/205/254/346/226/207/346/240/274/345/274/217.md +422 -0
  125. package/skills/core/official-doc-writer/scripts/__pycache__/generate_official_doc.cpython-313.pyc +0 -0
  126. package/skills/core/official-doc-writer/scripts/dialog_manager.py +564 -0
  127. package/skills/core/official-doc-writer/scripts/generate_official_doc.py +252 -0
  128. package/skills/core/official-doc-writer/scripts/install_fonts.py +390 -0
  129. package/skills/core/official-doc-writer/scripts/smart_prompts.py +363 -0
  130. package/skills/core/pdd-ba/SKILL.md +305 -0
  131. package/skills/core/pdd-ba/_meta.json +1 -0
  132. package/skills/core/pdd-ba/evals/default-evals.json +1 -0
  133. package/skills/core/pdd-code-reviewer/SKILL.md +378 -0
  134. package/skills/core/pdd-code-reviewer/_meta.json +1 -0
  135. package/skills/core/pdd-code-reviewer/evals/default-evals.json +1 -0
  136. package/skills/core/pdd-doc-change/SKILL.md +350 -0
  137. package/skills/core/pdd-doc-change/_meta.json +1 -0
  138. package/skills/core/pdd-doc-change/evals/default-evals.json +1 -0
  139. package/skills/core/pdd-doc-gardener/SKILL.md +248 -0
  140. package/skills/core/pdd-doc-gardener/_meta.json +1 -0
  141. package/skills/core/pdd-doc-gardener/evals/default-evals.json +1 -0
  142. package/skills/core/pdd-entropy-reduction/SKILL.md +360 -0
  143. package/skills/core/pdd-entropy-reduction/_meta.json +1 -0
  144. package/skills/core/pdd-entropy-reduction/evals/default-evals.json +1 -0
  145. package/skills/core/pdd-entropy-reduction/references/entropy-report-template.md +287 -0
  146. package/skills/core/pdd-entropy-reduction/references/golden-principles.md +573 -0
  147. package/skills/core/pdd-entropy-reduction/scripts/entropy_scan.py +712 -0
  148. package/skills/core/pdd-extract-features/SKILL.md +320 -0
  149. package/skills/core/pdd-extract-features/_meta.json +1 -0
  150. package/skills/core/pdd-extract-features/evals/default-evals.json +1 -0
  151. package/skills/core/pdd-generate-spec/SKILL.md +418 -0
  152. package/skills/core/pdd-generate-spec/_meta.json +1 -0
  153. package/skills/core/pdd-generate-spec/evals/default-evals.json +1 -0
  154. package/skills/core/pdd-implement-feature/SKILL.md +332 -0
  155. package/skills/core/pdd-implement-feature/_meta.json +1 -0
  156. package/skills/core/pdd-implement-feature/evals/default-evals.json +1 -0
  157. package/skills/core/pdd-main/SKILL.md +540 -0
  158. package/skills/core/pdd-main/_meta.json +1 -0
  159. package/skills/core/pdd-main/evals/default-evals.json +1 -0
  160. package/skills/core/pdd-main/evals/evals.json +215 -0
  161. package/skills/core/pdd-verify-feature/SKILL.md +474 -0
  162. package/skills/core/pdd-verify-feature/_meta.json +1 -0
  163. package/skills/core/pdd-verify-feature/evals/default-evals.json +1 -0
  164. package/skills/core/pdd-vm/evals/default-evals.json +1 -0
  165. package/skills/core/traffic-accident-assessor/LICENSE +29 -0
  166. package/skills/core/traffic-accident-assessor/SKILL.md +439 -0
  167. package/skills/core/traffic-accident-assessor/evals/evals.json +1 -0
  168. package/skills/core/traffic-accident-assessor/references/accident-types.md +369 -0
  169. package/skills/core/traffic-accident-assessor/references/liability-rules.md +287 -0
  170. package/skills/core/traffic-accident-assessor/references/traffic-laws.md +226 -0
  171. package/skills/core/traffic-accident-assessor/references//351/253/230/345/260/224/345/244/253/350/257/264/346/230/216/344/271/246.pdf +32576 -106
  172. package/skills/core/traffic-accident-assessor/scripts/generate_official_statement.py +588 -0
  173. package/skills/core/traffic-accident-assessor/scripts/generate_report.py +495 -0
  174. package/skills/core/traffic-accident-assessor/scripts/generate_statement.py +528 -0
  175. package/skills/core/traffic-accident-assessor.zip +0 -0
  176. package/skills/entropy/expert-arch-enforcer/SKILL.md +292 -0
  177. package/skills/entropy/expert-arch-enforcer/_meta.json +1 -0
  178. package/skills/entropy/expert-arch-enforcer/evals/default-evals.json +1 -0
  179. package/skills/entropy/expert-auto-refactor/SKILL.md +327 -0
  180. package/skills/entropy/expert-auto-refactor/_meta.json +1 -0
  181. package/skills/entropy/expert-auto-refactor/evals/default-evals.json +1 -0
  182. package/skills/entropy/expert-code-quality/SKILL.md +468 -0
  183. package/skills/entropy/expert-code-quality/_meta.json +1 -0
  184. package/skills/entropy/expert-code-quality/evals/default-evals.json +1 -0
  185. package/skills/entropy/expert-code-quality/evals/evals.json +109 -0
  186. package/skills/entropy/expert-code-quality/references/code-smells.md +605 -0
  187. package/skills/entropy/expert-code-quality/references/design-patterns.md +1111 -0
  188. package/skills/entropy/expert-code-quality/references/refactoring-catalog.md +1281 -0
  189. package/skills/entropy/expert-code-quality/references/solid-principles.md +524 -0
  190. package/skills/entropy/expert-entropy-auditor/SKILL.md +276 -0
  191. package/skills/entropy/expert-entropy-auditor/_meta.json +1 -0
  192. package/skills/entropy/expert-entropy-auditor/evals/default-evals.json +1 -0
  193. package/skills/expert/expert-activiti/SKILL.md +497 -0
  194. package/skills/expert/expert-activiti/_meta.json +1 -0
  195. package/skills/expert/expert-mysql/SKILL.md +832 -0
  196. package/skills/expert/expert-mysql/_meta.json +1 -0
  197. package/skills/expert/expert-performance/SKILL.md +379 -0
  198. package/skills/expert/expert-performance/_meta.json +1 -0
  199. package/skills/expert/expert-performance/evals/default-evals.json +1 -0
  200. package/skills/expert/expert-ruoyi/SKILL.md +472 -0
  201. package/skills/expert/expert-ruoyi/_meta.json +1 -0
  202. package/skills/expert/expert-security/SKILL.md +1341 -0
  203. package/skills/expert/expert-security/_meta.json +1 -0
  204. package/skills/expert/expert-security/evals/default-evals.json +1 -0
  205. package/skills/expert/software-architect/SKILL.md +350 -0
  206. package/skills/expert/software-architect/_meta.json +1 -0
  207. package/skills/expert/software-engineer/SKILL.md +437 -0
  208. package/skills/expert/software-engineer/_meta.json +1 -0
  209. package/skills/expert/software-engineer/architecture.md +130 -0
  210. package/skills/expert/software-engineer/patterns.md +151 -0
  211. package/skills/expert/software-engineer/testing.md +135 -0
  212. package/skills/expert/system-architect/SKILL.md +628 -0
  213. package/skills/expert/system-architect/_meta.json +1 -0
  214. package/skills/expert/system-architect/assets/templates/ARCHITECTURE.md +25 -0
  215. package/skills/expert/system-architect/assets/templates/README.md +44 -0
  216. package/skills/expert/system-architect/references/js-ts-standards.md +18 -0
  217. package/skills/expert/system-architect/references/python-standards.md +19 -0
  218. package/skills/expert/system-architect/references/scaffolding.md +61 -0
  219. package/skills/expert/system-architect/references/security-checklist.md +21 -0
  220. package/skills/openspec/openspec-apply-change/SKILL.md +156 -0
  221. package/skills/openspec/openspec-apply-change/_meta.json +1 -0
  222. package/skills/openspec/openspec-archive-change/SKILL.md +114 -0
  223. package/skills/openspec/openspec-archive-change/_meta.json +1 -0
  224. package/skills/openspec/openspec-bulk-archive-change/SKILL.md +246 -0
  225. package/skills/openspec/openspec-bulk-archive-change/_meta.json +1 -0
  226. package/skills/openspec/openspec-continue-change/SKILL.md +118 -0
  227. package/skills/openspec/openspec-continue-change/_meta.json +1 -0
  228. package/skills/openspec/openspec-explore/SKILL.md +288 -0
  229. package/skills/openspec/openspec-explore/_meta.json +1 -0
  230. package/skills/openspec/openspec-ff-change/SKILL.md +101 -0
  231. package/skills/openspec/openspec-ff-change/_meta.json +1 -0
  232. package/skills/openspec/openspec-new-change/SKILL.md +74 -0
  233. package/skills/openspec/openspec-new-change/_meta.json +1 -0
  234. package/skills/openspec/openspec-onboard/SKILL.md +554 -0
  235. package/skills/openspec/openspec-onboard/_meta.json +1 -0
  236. package/skills/openspec/openspec-sync-specs/SKILL.md +138 -0
  237. package/skills/openspec/openspec-sync-specs/_meta.json +1 -0
  238. package/skills/openspec/openspec-verify-change/SKILL.md +168 -0
  239. package/skills/openspec/openspec-verify-change/_meta.json +1 -0
  240. package/skills/pr/pdd-multi-review/SKILL.md +534 -0
  241. package/skills/pr/pdd-multi-review/_meta.json +1 -0
  242. package/skills/pr/pdd-pr-batch/SKILL.md +303 -0
  243. package/skills/pr/pdd-pr-batch/_meta.json +1 -0
  244. package/skills/pr/pdd-pr-create/SKILL.md +344 -0
  245. package/skills/pr/pdd-pr-create/_meta.json +1 -0
  246. package/skills/pr/pdd-pr-merge/SKILL.md +286 -0
  247. package/skills/pr/pdd-pr-merge/_meta.json +1 -0
  248. package/skills/pr/pdd-pr-review/SKILL.md +217 -0
  249. package/skills/pr/pdd-pr-review/_meta.json +1 -0
  250. package/skills/pr/pdd-task-manager/SKILL.md +636 -0
  251. package/skills/pr/pdd-task-manager/_meta.json +1 -0
  252. package/skills/pr/pdd-template-engine/SKILL.md +306 -0
  253. package/skills/pr/pdd-template-engine/_meta.json +1 -0
  254. package/templates/behavior-shaping/iron-law-template.md +87 -0
  255. package/templates/behavior-shaping/rationalization-template.md +62 -0
  256. package/templates/behavior-shaping/red-flags-template.md +70 -0
  257. package/templates/bilingual-template.md +139 -0
  258. package/templates/config/default.yaml +47 -0
  259. package/templates/project/default/README.md +31 -0
  260. package/templates/project/frontend/README.md +46 -0
  261. package/templates/project/java/README.md +48 -0
@@ -0,0 +1,759 @@
1
+ """
2
+ PDD SDK 工具函数模块
3
+
4
+ 提供重试机制、日志工具、缓存装饰器和格式化输出等通用功能。
5
+ 所有功能均基于 Python 标准库实现,无第三方依赖。
6
+ """
7
+
8
+ import functools
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ import sys
13
+ import time
14
+ from datetime import datetime
15
+ from typing import Any, Callable, Dict, List, Optional, Type, Tuple, Union
16
+
17
+
18
+ # ==================== 日志工具 ====================
19
+
20
+ # ANSI 颜色代码(用于控制台彩色输出)
21
+ class Colors:
22
+ """终端颜色常量"""
23
+ RESET = "\033[0m"
24
+ BOLD = "\033[1m"
25
+ DIM = "\033[2m"
26
+ RED = "\033[91m"
27
+ GREEN = "\033[92m"
28
+ YELLOW = "\033[93m"
29
+ BLUE = "\033[94m"
30
+ MAGENTA = "\033[95m"
31
+ CYAN = "\033[96m"
32
+ WHITE = "\033[97m"
33
+
34
+ # 检测是否支持颜色输出
35
+ @staticmethod
36
+ def supports_color() -> bool:
37
+ """检测当前终端是否支持彩色输出"""
38
+ # Windows 需要特殊处理
39
+ if sys.platform == "win32":
40
+ try:
41
+ import ctypes
42
+ kernel32 = ctypes.windll.kernel32
43
+ return kernel32.GetStdHandle(-11) != 0
44
+ except Exception:
45
+ return False
46
+ # Unix-like 系统:检查是否为 TTY
47
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
48
+
49
+
50
+ class ColoredFormatter(logging.Formatter):
51
+ """
52
+ 彩色日志格式化器
53
+
54
+ 根据日志级别自动选择不同的颜色输出。
55
+ """
56
+
57
+ # 级别到颜色的映射
58
+ LEVEL_COLORS = {
59
+ logging.DEBUG: Colors.DIM,
60
+ logging.INFO: Colors.GREEN,
61
+ logging.WARNING: Colors.YELLOW,
62
+ logging.ERROR: Colors.RED,
63
+ logging.CRITICAL: Colors.BOLD + Colors.RED,
64
+ }
65
+
66
+ def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None):
67
+ """
68
+ 初始化格式化器
69
+
70
+ Args:
71
+ fmt: 日志格式字符串
72
+ datefmt: 时间格式字符串
73
+ """
74
+ super().__init__(fmt=fmt, datefmt=datefmt)
75
+ self._use_color = Colors.supports_color()
76
+
77
+ def format(self, record: logging.LogRecord) -> str:
78
+ """
79
+ 格式化日志记录
80
+
81
+ Args:
82
+ record: 日志记录对象
83
+
84
+ Returns:
85
+ 格式化后的字符串
86
+ """
87
+ if self._use_color:
88
+ color = self.LEVEL_COLORS.get(record.levelno, Colors.RESET)
89
+ record.levelname = f"{color}{record.levelname}{Colors.RESET}"
90
+
91
+ # 为不同级别添加图标前缀
92
+ icons = {
93
+ logging.DEBUG: "[DEBUG]",
94
+ logging.INFO: "[INFO]",
95
+ logging.WARNING: "[WARN]",
96
+ logging.ERROR: "[ERROR]",
97
+ logging.CRITICAL: "[FATAL]",
98
+ }
99
+ icon = icons.get(record.levelno, "")
100
+ record.msg = f"{icon} {record.msg}"
101
+
102
+ return super().format(record)
103
+
104
+
105
+ def get_logger(
106
+ name: str = "pdd_sdk",
107
+ level: int = logging.INFO,
108
+ enable_color: bool = True
109
+ ) -> logging.Logger:
110
+ """
111
+ 获取配置好的日志器实例
112
+
113
+ 创建或返回已存在的日志器,支持彩色控制台输出。
114
+
115
+ Args:
116
+ name: 日志器名称,通常使用模块名
117
+ level: 日志级别 (DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50)
118
+ enable_color: 是否启用彩色输出
119
+
120
+ Returns:
121
+ 配置完成的 Logger 实例
122
+
123
+ Example:
124
+ >>> logger = get_logger("my_module", level=logging.DEBUG)
125
+ >>> logger.info("这是一条信息")
126
+ [INFO] 这是一条信息
127
+ >>> logger.error("出错了")
128
+ [ERROR] 出错了
129
+ """
130
+ logger = logging.getLogger(name)
131
+
132
+ # 避免重复添加处理器
133
+ if logger.handlers:
134
+ logger.setLevel(level)
135
+ return logger
136
+
137
+ logger.setLevel(level)
138
+
139
+ # 控制台处理器
140
+ console_handler = logging.StreamHandler(sys.stdout)
141
+ console_handler.setLevel(level)
142
+
143
+ # 设置格式化器
144
+ log_format = "%(asctime)s | %(levelname)-18s | %(message)s"
145
+ date_format = "%H:%M:%S"
146
+
147
+ if enable_color and Colors.supports_color():
148
+ formatter = ColoredFormatter(fmt=log_format, datefmt=date_format)
149
+ else:
150
+ formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
151
+
152
+ console_handler.setFormatter(formatter)
153
+ logger.addHandler(console_handler)
154
+
155
+ # 不向上传播,避免重复输出
156
+ logger.propagate = False
157
+
158
+ return logger
159
+
160
+
161
+ # ==================== 重试装饰器 ====================
162
+
163
+ def retry(
164
+ max_retries: int = 3,
165
+ delay: float = 1.0,
166
+ backoff: float = 2.0,
167
+ exceptions: Tuple[Type[Exception], ...] = (Exception,),
168
+ on_retry: Optional[Callable[[int, Exception], None]] = None,
169
+ jitter: bool = True
170
+ ):
171
+ """
172
+ 重试装饰器
173
+
174
+ 在函数执行失败时自动重试,支持指数退避策略。
175
+
176
+ Args:
177
+ max_retries: 最大重试次数(不包括首次尝试)
178
+ delay: 初始延迟时间(秒)
179
+ backoff: 退避倍数,每次重试延迟乘以此值
180
+ exceptions: 需要重试的异常类型元组
181
+ on_retry: 重试时的回调函数,接收(尝试次数, 异常)参数
182
+ jitter: 是否在延迟中添加随机抖动,避免惊群效应
183
+
184
+ Example:
185
+ >>> @retry(max_retries=3, delay=1.0, backoff=2.0, exceptions=(ConnectionError,))
186
+ ... async def fetch_data():
187
+ ... # 可能失败的网络请求
188
+ ... pass
189
+ """
190
+ import random
191
+
192
+ def decorator(func: Callable) -> Callable:
193
+ @functools.wraps(func)
194
+ async def async_wrapper(*args, **kwargs):
195
+ last_exception = None
196
+ current_delay = delay
197
+
198
+ for attempt in range(max_retries + 1):
199
+ try:
200
+ return await func(*args, **kwargs)
201
+ except exceptions as e:
202
+ last_exception = e
203
+
204
+ if attempt < max_retries:
205
+ # 调用重试回调
206
+ if on_retry:
207
+ on_retry(attempt + 1, e)
208
+
209
+ # 计算带抖动的延迟
210
+ actual_delay = current_delay
211
+ if jitter:
212
+ actual_delay += random.uniform(0, current_delay * 0.1)
213
+
214
+ # 等待后重试
215
+ await _async_sleep(actual_delay)
216
+
217
+ # 指数退避
218
+ current_delay *= backoff
219
+ else:
220
+ raise
221
+
222
+ raise last_exception
223
+
224
+ @functools.wraps(func)
225
+ def sync_wrapper(*args, **kwargs):
226
+ last_exception = None
227
+ current_delay = delay
228
+
229
+ for attempt in range(max_retries + 1):
230
+ try:
231
+ return func(*args, **kwargs)
232
+ except exceptions as e:
233
+ last_exception = e
234
+
235
+ if attempt < max_retries:
236
+ if on_retry:
237
+ on_retry(attempt + 1, e)
238
+
239
+ actual_delay = current_delay
240
+ if jitter:
241
+ actual_delay += random.uniform(0, current_delay * 0.1)
242
+
243
+ time.sleep(actual_delay)
244
+ current_delay *= backoff
245
+ else:
246
+ raise
247
+
248
+ raise last_exception
249
+
250
+ # 自动检测函数是同步还是异步
251
+ if asyncio.iscoroutinefunction(func):
252
+ return async_wrapper
253
+ else:
254
+ return sync_wrapper
255
+
256
+ return decorator
257
+
258
+
259
+ async def _async_sleep(seconds: float) -> None:
260
+ """异步睡眠辅助函数"""
261
+ import asyncio
262
+ await asyncio.sleep(seconds)
263
+
264
+
265
+ # ==================== 缓存装饰器 ====================
266
+
267
+ class _CacheEntry:
268
+ """缓存条目内部类"""
269
+ __slots__ = ("value", "expires_at")
270
+
271
+ def __init__(self, value: Any, expires_at: float):
272
+ self.value = value
273
+ self.expires_at = expires_at
274
+
275
+ @property
276
+ def is_expired(self) -> bool:
277
+ return time.time() > self.expires_at
278
+
279
+
280
+ class MemoryCache:
281
+ """
282
+ 内存缓存管理器
283
+
284
+ 基于 TTL 的简单内存缓存实现,线程安全。
285
+
286
+ Attributes:
287
+ _cache: 缓存存储字典
288
+ _default_ttl: 默认过期时间(秒)
289
+ _max_size: 最大缓存条目数
290
+ """
291
+
292
+ def __init__(self, default_ttl: int = 300, max_size: int = 1000):
293
+ """
294
+ 初始化缓存
295
+
296
+ Args:
297
+ default_ttl: 默认 TTL(秒),默认 5 分钟
298
+ max_size: 最大缓存条目数,防止内存泄漏
299
+ """
300
+ self._cache: Dict[str, _CacheEntry] = {}
301
+ self._default_ttl = default_ttl
302
+ self._max_size = max_size
303
+ self._lock = __import__("threading").Lock()
304
+ self._hits = 0
305
+ self._misses = 0
306
+
307
+ def get(self, key: str) -> Optional[Any]:
308
+ """
309
+ 获取缓存值
310
+
311
+ Args:
312
+ key: 缓存键
313
+
314
+ Returns:
315
+ 缓存的值,如果不存在或已过期则返回 None
316
+ """
317
+ with self._lock:
318
+ entry = self._cache.get(key)
319
+
320
+ if entry is None:
321
+ self._misses += 1
322
+ return None
323
+
324
+ if entry.is_expired:
325
+ del self._cache[key]
326
+ self._misses += 1
327
+ return None
328
+
329
+ self._hits += 1
330
+ return entry.value
331
+
332
+ def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
333
+ """
334
+ 设置缓存值
335
+
336
+ Args:
337
+ key: 缓存键
338
+ value: 要缓存的值
339
+ ttl: 过期时间(秒),None 则使用默认值
340
+ """
341
+ with self._lock:
342
+ # 如果超过最大容量,清理最旧的条目
343
+ if len(self._cache) >= self._max_size and key not in self._cache:
344
+ self._evict_oldest()
345
+
346
+ expiration = ttl or self._default_ttl
347
+ self._cache[key] = _CacheEntry(value, time.time() + expiration)
348
+
349
+ def delete(self, key: str) -> bool:
350
+ """
351
+ 删除缓存条目
352
+
353
+ Args:
354
+ key: 要删除的键
355
+
356
+ Returns:
357
+ 是否成功删除
358
+ """
359
+ with self._lock:
360
+ if key in self._cache:
361
+ del self._cache[key]
362
+ return True
363
+ return False
364
+
365
+ def clear(self) -> None:
366
+ """清空所有缓存"""
367
+ with self._lock:
368
+ self._cache.clear()
369
+
370
+ def has(self, key: str) -> bool:
371
+ """
372
+ 检查键是否存在且未过期
373
+
374
+ Args:
375
+ key: 缓存键
376
+
377
+ Returns:
378
+ 是否存在有效缓存
379
+ """
380
+ with self._lock:
381
+ entry = self._cache.get(key)
382
+ if entry is None or entry.is_expired:
383
+ return False
384
+ return True
385
+
386
+ @property
387
+ def size(self) -> int:
388
+ """返回当前缓存条目数"""
389
+ with self._lock:
390
+ return len(self._cache)
391
+
392
+ @property
393
+ def stats(self) -> Dict[str, Any]:
394
+ """返回缓存统计信息"""
395
+ total = self._hits + self._misses
396
+ hit_rate = (self._hits / total * 100) if total > 0 else 0
397
+ return {
398
+ "size": self.size,
399
+ "max_size": self._max_size,
400
+ "hits": self._hits,
401
+ "misses": self._misses,
402
+ "hit_rate": f"{hit_rate:.2f}%"
403
+ }
404
+
405
+ def _evict_oldest(self) -> None:
406
+ """淘汰最早的缓存条目"""
407
+ if not self._cache:
408
+ return
409
+ oldest_key = min(
410
+ self._cache.keys(),
411
+ key=lambda k: self._cache[k].expires_at
412
+ )
413
+ del self._cache[oldest_key]
414
+
415
+ def cleanup_expired(self) -> int:
416
+ """
417
+ 清理所有过期的缓存条目
418
+
419
+ Returns:
420
+ 清理的条目数量
421
+ """
422
+ with self._lock:
423
+ expired_keys = [
424
+ k for k, v in self._cache.items() if v.is_expired
425
+ ]
426
+ for key in expired_keys:
427
+ del self._cache[key]
428
+ return len(expired_keys)
429
+
430
+
431
+ # 全局默认缓存实例
432
+ _default_cache = MemoryCache()
433
+
434
+
435
+ def cache(ttl: int = 300, key_fn: Optional[Callable] = None):
436
+ """
437
+ 缓存装饰器
438
+
439
+ 基于函数参数和 TTL 的结果缓存。
440
+
441
+ Args:
442
+ ttl: 缓存有效期(秒),默认 5 分钟
443
+ key_fn: 自定义缓存键生成函数,接收(*args, **kwargs),返回字符串
444
+
445
+ Example:
446
+ >>> @cache(ttl=60)
447
+ ... async def get_user(user_id: str) -> dict:
448
+ ... # 数据库查询...
449
+ ... return {"id": user_id, "name": "test"}
450
+ """
451
+ def decorator(func: Callable) -> Callable:
452
+ @functools.wraps(func)
453
+ async def async_wrapper(*args, **kwargs):
454
+ # 生成缓存键
455
+ cache_key = _make_cache_key(func.__name__, args, kwargs, key_fn)
456
+
457
+ # 尝试从缓存获取
458
+ cached = _default_cache.get(cache_key)
459
+ if cached is not None:
460
+ return cached
461
+
462
+ # 执行函数并缓存结果
463
+ result = await func(*args, **kwargs)
464
+ _default_cache.set(cache_key, result, ttl)
465
+ return result
466
+
467
+ @functools.wraps(func)
468
+ def sync_wrapper(*args, **kwargs):
469
+ cache_key = _make_cache_key(func.__name__, args, kwargs, key_fn)
470
+
471
+ cached = _default_cache.get(cache_key)
472
+ if cached is not None:
473
+ return cached
474
+
475
+ result = func(*args, **kwargs)
476
+ _default_cache.set(cache_key, result, ttl)
477
+ return result
478
+
479
+ if asyncio.iscoroutinefunction(func):
480
+ return async_wrapper
481
+ else:
482
+ return sync_wrapper
483
+
484
+ return decorator
485
+
486
+
487
+ def _make_cache_key(
488
+ func_name: str,
489
+ args: tuple,
490
+ kwargs: dict,
491
+ key_fn: Optional[Callable] = None
492
+ ) -> str:
493
+ """
494
+ 生成缓存键
495
+
496
+ Args:
497
+ func_name: 函数名
498
+ args: 位置参数
499
+ kwargs: 关键字参数
500
+ key_fn: 自定义键生成函数
501
+
502
+ Returns:
503
+ 缓存键字符串
504
+ """
505
+ if key_fn:
506
+ custom_key = key_fn(*args, **kwargs)
507
+ if isinstance(custom_key, str):
508
+ return custom_key
509
+
510
+ # 默认键生成方式:哈希参数
511
+ key_data = {
512
+ "func": func_name,
513
+ "args": str(args),
514
+ "kwargs": str(sorted(kwargs.items()))
515
+ }
516
+ key_string = json.dumps(key_data, sort_keys=True, ensure_ascii=False)
517
+ return hashlib.md5(key_string.encode()).hexdigest()
518
+
519
+
520
+ # ==================== 格式化输出工具 ====================
521
+
522
+
523
+ def format_table(
524
+ data: List[Dict[str, Any]],
525
+ columns: Optional[List[str]] = None,
526
+ title: Optional[str] = None,
527
+ show_index: bool = True
528
+ ) -> str:
529
+ """
530
+ 将数据格式化为对齐的表格字符串
531
+
532
+ Args:
533
+ data: 字典列表,每个字典代表一行数据
534
+ columns: 要显示的列名列表,None 则显示所有列
535
+ title: 表格标题(可选)
536
+ show_index: 是否显示行号
537
+
538
+ Returns:
539
+ 格式化的表格字符串
540
+
541
+ Example:
542
+ >>> data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
543
+ >>> print(format_table(data))
544
+ # | name | age
545
+ # ---+-------+-----
546
+ # 0 | Alice | 30
547
+ # 1 | Bob | 25
548
+ """
549
+ if not data:
550
+ return "(空表格)"
551
+
552
+ # 确定要显示的列
553
+ if columns is None:
554
+ columns = list(data[0].keys())
555
+
556
+ # 计算每列宽度
557
+ col_widths: Dict[str, int] = {}
558
+ for col in col_name in columns:
559
+ header_len = len(str(col))
560
+ max_value_len = max(len(str(row.get(col, ""))) for row in data)
561
+ col_widths[col] = max(header_len, max_value_len)
562
+
563
+ # 如果显示行号,增加索引列宽度
564
+ index_width = len(str(len(data))) if show_index else 0
565
+
566
+ lines = []
567
+
568
+ # 标题
569
+ if title:
570
+ lines.append(title)
571
+ total_width = index_width + sum(col_widths.values()) + len(columns) * 3 + (1 if show_index else 0)
572
+ lines.append("=" * total_width)
573
+
574
+ # 表头
575
+ header_parts = []
576
+ if show_index:
577
+ header_parts.append(f"{'#' :^{index_width}}")
578
+ for col in columns:
579
+ header_parts.append(f" {str(col):^{col_widths[col]}} ")
580
+ lines.append("|".join(header_parts))
581
+
582
+ # 分隔线
583
+ sep_parts = []
584
+ if show_index:
585
+ sep_parts.append("-" * index_width)
586
+ for col in columns:
587
+ sep_parts.append("-" * (col_widths[col] + 2))
588
+ lines.append("+".join(sep_parts))
589
+
590
+ # 数据行
591
+ for idx, row in enumerate(data):
592
+ row_parts = []
593
+ if show_index:
594
+ row_parts.append(f"{idx :>{index_width}}")
595
+ for col in columns:
596
+ value = row.get(col, "")
597
+ row_parts.append(f" {str(value):>{col_widths[col]}} ")
598
+ lines.append("|".join(row_parts))
599
+
600
+ return "\n".join(lines)
601
+
602
+
603
+ def format_json(
604
+ data: Any,
605
+ indent: int = 2,
606
+ ensure_ascii: bool = False,
607
+ sort_keys: bool = False
608
+ ) -> str:
609
+ """
610
+ JSON 美化输出
611
+
612
+ Args:
613
+ data: 要序列化的数据
614
+ indent: 缩进空格数
615
+ ensure_ascii: 是否转义非 ASCII 字符
616
+ sort_keys: 是否排序键
617
+
618
+ Returns:
619
+ 格式化的 JSON 字符串
620
+ """
621
+ return json.dumps(
622
+ data,
623
+ indent=indent,
624
+ ensure_ascii=ensure_ascii,
625
+ sort_keys=sort_keys,
626
+ default=str
627
+ )
628
+
629
+
630
+ def format_duration(ms: int) -> str:
631
+ """
632
+ 格式化持续时间
633
+
634
+ 将毫秒数转换为人类可读的时间字符串。
635
+
636
+ Args:
637
+ ms: 持续时间(毫秒)
638
+
639
+ Returns:
640
+ 格式化的时间字符串
641
+
642
+ Example:
643
+ >>> format_duration(1500)
644
+ '1.50s'
645
+ >>> format_duration(500)
646
+ '500ms'
647
+ >>> format_duration(65000)
648
+ '1m 5s'
649
+ """
650
+ if ms < 1000:
651
+ return f"{ms}ms"
652
+ elif ms < 60000:
653
+ return f"{ms / 1000:.2f}s"
654
+ else:
655
+ minutes = ms // 60000
656
+ seconds = (ms % 60000) / 1000
657
+ return f"{minutes}m {seconds:.0f}s"
658
+
659
+
660
+ def format_bytes(size: int) -> str:
661
+ """
662
+ 格式化字节数
663
+
664
+ 将字节转换为人类可读的大小表示。
665
+
666
+ Args:
667
+ size: 字节数
668
+
669
+ Returns:
670
+ 格式化的大小字符串
671
+
672
+ Example:
673
+ >>> format_bytes(1024)
674
+ '1.00 KB'
675
+ >>> format_bytes(1048576)
676
+ '1.00 MB'
677
+ """
678
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
679
+ if abs(size) < 1024.0:
680
+ return f"{size:.2f} {unit}"
681
+ size /= 1024.0
682
+ return f"{size:.2f} PB"
683
+
684
+
685
+ def truncate(text: str, max_length: int = 80, suffix: str = "...") -> str:
686
+ """
687
+ 截断文本
688
+
689
+ 如果文本超过指定长度,截断并添加后缀。
690
+
691
+ Args:
692
+ text: 原始文本
693
+ max_length: 最大长度
694
+ suffix: 截断后缀
695
+
696
+ Returns:
697
+ 处理后的文本
698
+ """
699
+ if len(text) <= max_length:
700
+ return text
701
+ return text[:max_length - len(suffix)] + suffix
702
+
703
+
704
+ # ==================== 验证工具 ====================
705
+
706
+
707
+ def validate_endpoint(endpoint: str) -> str:
708
+ """
709
+ 验证并规范化 API 端点 URL
710
+
711
+ Args:
712
+ endpoint: 输入的端点 URL
713
+
714
+ Returns:
715
+ 规范化后的 URL
716
+
717
+ Raises:
718
+ ValueError: 如果 URL 格式无效
719
+ """
720
+ from urllib.parse import urlparse
721
+
722
+ endpoint = endpoint.strip().rstrip("/")
723
+
724
+ parsed = urlparse(endpoint)
725
+ if not all([parsed.scheme, parsed.netloc]):
726
+ raise ValueError(
727
+ f"无效的端点 URL: '{endpoint}'。"
728
+ f"请提供完整的 URL,如 'http://localhost:3000'"
729
+ )
730
+
731
+ if parsed.scheme not in ("http", "https"):
732
+ raise ValueError(
733
+ f"不支持的协议 '{parsed.scheme}',仅支持 http 和 https"
734
+ )
735
+
736
+ return endpoint
737
+
738
+
739
+ def validate_api_key(api_key: str) -> str:
740
+ """
741
+ 验证 API Key
742
+
743
+ Args:
744
+ api_key: API Key 字符串
745
+
746
+ Returns:
747
+ 去除首尾空白的 API Key
748
+
749
+ Raises:
750
+ ValueError: 如果 API Key 为空或仅包含空白字符
751
+ """
752
+ api_key = api_key.strip()
753
+ if not api_key:
754
+ raise ValueError("API Key 不能为空")
755
+ return api_key
756
+
757
+
758
+ # 导入 asyncio 用于检测协程函数
759
+ import asyncio