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.
- package/README.md +1478 -0
- package/bin/pdd.js +354 -0
- package/config/bpmn-rules.yaml +166 -0
- package/config/checkstyle.xml +105 -0
- package/config/eslint.config.js +48 -0
- package/config/pmd.xml +91 -0
- package/config/prd-rules.yaml +113 -0
- package/config/ruff.toml +45 -0
- package/config/sqlfluff.cfg +82 -0
- package/hooks/hook-executor.js +332 -0
- package/index.js +43 -0
- package/lib/api-routes.js +750 -0
- package/lib/api-server.js +408 -0
- package/lib/cache/cache-config.js +209 -0
- package/lib/cache/system-cache.js +852 -0
- package/lib/config-manager.js +373 -0
- package/lib/generate.js +528 -0
- package/lib/grpc/grpc-routes.js +1134 -0
- package/lib/grpc/grpc-server.js +912 -0
- package/lib/grpc/proto-definitions.js +1033 -0
- package/lib/init.js +172 -0
- package/lib/iteration/auto-fixer.js +1025 -0
- package/lib/iteration/auto-reviewer.js +923 -0
- package/lib/iteration/controller.js +577 -0
- package/lib/list.js +130 -0
- package/lib/mcp-server.js +548 -0
- package/lib/openclaw/api-integration.js +535 -0
- package/lib/openclaw/cli-integration.js +567 -0
- package/lib/openclaw/data-sync.js +845 -0
- package/lib/openclaw/openclaw-adapter.js +783 -0
- package/lib/plugin/example-plugins/code-stats/index.js +332 -0
- package/lib/plugin/example-plugins/code-stats/plugin.json +1 -0
- package/lib/plugin/example-plugins/custom-linter/index.js +472 -0
- package/lib/plugin/example-plugins/custom-linter/plugin.json +1 -0
- package/lib/plugin/example-plugins/hello-world/index.js +86 -0
- package/lib/plugin/example-plugins/hello-world/plugin.json +1 -0
- package/lib/plugin/plugin-manager.js +655 -0
- package/lib/plugin/plugin-sdk.js +565 -0
- package/lib/plugin/sandbox.js +627 -0
- package/lib/quality/rules/maintainability.js +418 -0
- package/lib/quality/rules/performance.js +498 -0
- package/lib/quality/rules/readability.js +441 -0
- package/lib/quality/rules/robustness.js +504 -0
- package/lib/quality/rules/security.js +444 -0
- package/lib/quality/scorer.js +576 -0
- package/lib/report.js +669 -0
- package/lib/sdk-base.js +301 -0
- package/lib/sdk-js.js +446 -0
- package/lib/sdk-python/README.md +546 -0
- package/lib/sdk-python/examples/basic_usage.py +450 -0
- package/lib/sdk-python/pdd_sdk/__init__.py +180 -0
- package/lib/sdk-python/pdd_sdk/client.py +1170 -0
- package/lib/sdk-python/pdd_sdk/events.py +423 -0
- package/lib/sdk-python/pdd_sdk/exceptions.py +158 -0
- package/lib/sdk-python/pdd_sdk/models.py +518 -0
- package/lib/sdk-python/pdd_sdk/utils.py +759 -0
- package/lib/token/budget-alert.js +367 -0
- package/lib/token/budget-manager.js +485 -0
- package/lib/update.js +54 -0
- package/lib/utils/logger.js +88 -0
- package/lib/verify.js +741 -0
- package/lib/version.js +52 -0
- package/lib/vm/README.md +102 -0
- package/lib/vm/dashboard/api-routes.js +669 -0
- package/lib/vm/dashboard/server.js +391 -0
- package/lib/vm/dashboard/sse.js +358 -0
- package/lib/vm/dashboard/static/css/dashboard.css +1378 -0
- package/lib/vm/dashboard/static/index.html +118 -0
- package/lib/vm/dashboard/static/js/app.js +949 -0
- package/lib/vm/dashboard/static/js/charts.js +913 -0
- package/lib/vm/dashboard/static/js/kanban-view.js +1053 -0
- package/lib/vm/dashboard/static/js/pipeline-view.js +463 -0
- package/lib/vm/dashboard/static/js/quality-view.js +598 -0
- package/lib/vm/dashboard/static/js/system-view.js +1021 -0
- package/lib/vm/data-provider.js +1191 -0
- package/lib/vm/event-bus.js +402 -0
- package/lib/vm/hooks/extract-hook.js +307 -0
- package/lib/vm/hooks/generate-hook.js +374 -0
- package/lib/vm/hooks/hook-interface.js +458 -0
- package/lib/vm/hooks/report-hook.js +331 -0
- package/lib/vm/hooks/verify-hook.js +454 -0
- package/lib/vm/models.js +1003 -0
- package/lib/vm/reconciler.js +855 -0
- package/lib/vm/scanner.js +988 -0
- package/lib/vm/state-schema.js +955 -0
- package/lib/vm/state-store.js +733 -0
- package/lib/vm/tui/components/card.js +339 -0
- package/lib/vm/tui/components/progress-bar.js +368 -0
- package/lib/vm/tui/components/sparkline.js +327 -0
- package/lib/vm/tui/components/status-light.js +294 -0
- package/lib/vm/tui/components/table.js +370 -0
- package/lib/vm/tui/input.js +335 -0
- package/lib/vm/tui/renderer.js +548 -0
- package/lib/vm/tui/screens/kanban-screen.js +397 -0
- package/lib/vm/tui/screens/overview-screen.js +357 -0
- package/lib/vm/tui/screens/quality-screen.js +336 -0
- package/lib/vm/tui/screens/system-screen.js +379 -0
- package/lib/vm/tui/tui.js +805 -0
- package/package.json +1 -0
- package/scripts/cso-analyzer.js +198 -0
- package/scripts/eval-runner.js +359 -0
- package/scripts/i18n-checker.js +109 -0
- package/scripts/linter/activiti-linter.js +272 -0
- package/scripts/linter/prd-linter.js +162 -0
- package/scripts/linter/report-generator.js +207 -0
- package/scripts/linter/run-linters.js +285 -0
- package/scripts/linter/sql-linter.js +166 -0
- package/scripts/token-analyzer.js +162 -0
- package/scripts/vm-test.js +180 -0
- package/skills/core/official-doc-writer/LICENSE +21 -0
- package/skills/core/official-doc-writer/README.md +232 -0
- package/skills/core/official-doc-writer/SKILL.md +475 -0
- package/skills/core/official-doc-writer/_meta.json +1 -0
- package/skills/core/official-doc-writer/document_generator.py +580 -0
- package/skills/core/official-doc-writer/evals/default-evals.json +1 -0
- package/skills/core/official-doc-writer/examples.md +150 -0
- package/skills/core/official-doc-writer/fonts/FONTS_LIST.md +45 -0
- package/skills/core/official-doc-writer/fonts/README.md +141 -0
- package/skills/core/official-doc-writer/fonts/SIMFANG.TTF +0 -0
- package/skills/core/official-doc-writer/fonts/SIMHEI.TTF +0 -0
- package/skills/core/official-doc-writer/fonts/SIMKAI.TTF +0 -0
- package/skills/core/official-doc-writer/fonts/SIMSUN.TTC +0 -0
- 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
- 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
- package/skills/core/official-doc-writer/scripts/__pycache__/generate_official_doc.cpython-313.pyc +0 -0
- package/skills/core/official-doc-writer/scripts/dialog_manager.py +564 -0
- package/skills/core/official-doc-writer/scripts/generate_official_doc.py +252 -0
- package/skills/core/official-doc-writer/scripts/install_fonts.py +390 -0
- package/skills/core/official-doc-writer/scripts/smart_prompts.py +363 -0
- package/skills/core/pdd-ba/SKILL.md +305 -0
- package/skills/core/pdd-ba/_meta.json +1 -0
- package/skills/core/pdd-ba/evals/default-evals.json +1 -0
- package/skills/core/pdd-code-reviewer/SKILL.md +378 -0
- package/skills/core/pdd-code-reviewer/_meta.json +1 -0
- package/skills/core/pdd-code-reviewer/evals/default-evals.json +1 -0
- package/skills/core/pdd-doc-change/SKILL.md +350 -0
- package/skills/core/pdd-doc-change/_meta.json +1 -0
- package/skills/core/pdd-doc-change/evals/default-evals.json +1 -0
- package/skills/core/pdd-doc-gardener/SKILL.md +248 -0
- package/skills/core/pdd-doc-gardener/_meta.json +1 -0
- package/skills/core/pdd-doc-gardener/evals/default-evals.json +1 -0
- package/skills/core/pdd-entropy-reduction/SKILL.md +360 -0
- package/skills/core/pdd-entropy-reduction/_meta.json +1 -0
- package/skills/core/pdd-entropy-reduction/evals/default-evals.json +1 -0
- package/skills/core/pdd-entropy-reduction/references/entropy-report-template.md +287 -0
- package/skills/core/pdd-entropy-reduction/references/golden-principles.md +573 -0
- package/skills/core/pdd-entropy-reduction/scripts/entropy_scan.py +712 -0
- package/skills/core/pdd-extract-features/SKILL.md +320 -0
- package/skills/core/pdd-extract-features/_meta.json +1 -0
- package/skills/core/pdd-extract-features/evals/default-evals.json +1 -0
- package/skills/core/pdd-generate-spec/SKILL.md +418 -0
- package/skills/core/pdd-generate-spec/_meta.json +1 -0
- package/skills/core/pdd-generate-spec/evals/default-evals.json +1 -0
- package/skills/core/pdd-implement-feature/SKILL.md +332 -0
- package/skills/core/pdd-implement-feature/_meta.json +1 -0
- package/skills/core/pdd-implement-feature/evals/default-evals.json +1 -0
- package/skills/core/pdd-main/SKILL.md +540 -0
- package/skills/core/pdd-main/_meta.json +1 -0
- package/skills/core/pdd-main/evals/default-evals.json +1 -0
- package/skills/core/pdd-main/evals/evals.json +215 -0
- package/skills/core/pdd-verify-feature/SKILL.md +474 -0
- package/skills/core/pdd-verify-feature/_meta.json +1 -0
- package/skills/core/pdd-verify-feature/evals/default-evals.json +1 -0
- package/skills/core/pdd-vm/evals/default-evals.json +1 -0
- package/skills/core/traffic-accident-assessor/LICENSE +29 -0
- package/skills/core/traffic-accident-assessor/SKILL.md +439 -0
- package/skills/core/traffic-accident-assessor/evals/evals.json +1 -0
- package/skills/core/traffic-accident-assessor/references/accident-types.md +369 -0
- package/skills/core/traffic-accident-assessor/references/liability-rules.md +287 -0
- package/skills/core/traffic-accident-assessor/references/traffic-laws.md +226 -0
- 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
- package/skills/core/traffic-accident-assessor/scripts/generate_official_statement.py +588 -0
- package/skills/core/traffic-accident-assessor/scripts/generate_report.py +495 -0
- package/skills/core/traffic-accident-assessor/scripts/generate_statement.py +528 -0
- package/skills/core/traffic-accident-assessor.zip +0 -0
- package/skills/entropy/expert-arch-enforcer/SKILL.md +292 -0
- package/skills/entropy/expert-arch-enforcer/_meta.json +1 -0
- package/skills/entropy/expert-arch-enforcer/evals/default-evals.json +1 -0
- package/skills/entropy/expert-auto-refactor/SKILL.md +327 -0
- package/skills/entropy/expert-auto-refactor/_meta.json +1 -0
- package/skills/entropy/expert-auto-refactor/evals/default-evals.json +1 -0
- package/skills/entropy/expert-code-quality/SKILL.md +468 -0
- package/skills/entropy/expert-code-quality/_meta.json +1 -0
- package/skills/entropy/expert-code-quality/evals/default-evals.json +1 -0
- package/skills/entropy/expert-code-quality/evals/evals.json +109 -0
- package/skills/entropy/expert-code-quality/references/code-smells.md +605 -0
- package/skills/entropy/expert-code-quality/references/design-patterns.md +1111 -0
- package/skills/entropy/expert-code-quality/references/refactoring-catalog.md +1281 -0
- package/skills/entropy/expert-code-quality/references/solid-principles.md +524 -0
- package/skills/entropy/expert-entropy-auditor/SKILL.md +276 -0
- package/skills/entropy/expert-entropy-auditor/_meta.json +1 -0
- package/skills/entropy/expert-entropy-auditor/evals/default-evals.json +1 -0
- package/skills/expert/expert-activiti/SKILL.md +497 -0
- package/skills/expert/expert-activiti/_meta.json +1 -0
- package/skills/expert/expert-mysql/SKILL.md +832 -0
- package/skills/expert/expert-mysql/_meta.json +1 -0
- package/skills/expert/expert-performance/SKILL.md +379 -0
- package/skills/expert/expert-performance/_meta.json +1 -0
- package/skills/expert/expert-performance/evals/default-evals.json +1 -0
- package/skills/expert/expert-ruoyi/SKILL.md +472 -0
- package/skills/expert/expert-ruoyi/_meta.json +1 -0
- package/skills/expert/expert-security/SKILL.md +1341 -0
- package/skills/expert/expert-security/_meta.json +1 -0
- package/skills/expert/expert-security/evals/default-evals.json +1 -0
- package/skills/expert/software-architect/SKILL.md +350 -0
- package/skills/expert/software-architect/_meta.json +1 -0
- package/skills/expert/software-engineer/SKILL.md +437 -0
- package/skills/expert/software-engineer/_meta.json +1 -0
- package/skills/expert/software-engineer/architecture.md +130 -0
- package/skills/expert/software-engineer/patterns.md +151 -0
- package/skills/expert/software-engineer/testing.md +135 -0
- package/skills/expert/system-architect/SKILL.md +628 -0
- package/skills/expert/system-architect/_meta.json +1 -0
- package/skills/expert/system-architect/assets/templates/ARCHITECTURE.md +25 -0
- package/skills/expert/system-architect/assets/templates/README.md +44 -0
- package/skills/expert/system-architect/references/js-ts-standards.md +18 -0
- package/skills/expert/system-architect/references/python-standards.md +19 -0
- package/skills/expert/system-architect/references/scaffolding.md +61 -0
- package/skills/expert/system-architect/references/security-checklist.md +21 -0
- package/skills/openspec/openspec-apply-change/SKILL.md +156 -0
- package/skills/openspec/openspec-apply-change/_meta.json +1 -0
- package/skills/openspec/openspec-archive-change/SKILL.md +114 -0
- package/skills/openspec/openspec-archive-change/_meta.json +1 -0
- package/skills/openspec/openspec-bulk-archive-change/SKILL.md +246 -0
- package/skills/openspec/openspec-bulk-archive-change/_meta.json +1 -0
- package/skills/openspec/openspec-continue-change/SKILL.md +118 -0
- package/skills/openspec/openspec-continue-change/_meta.json +1 -0
- package/skills/openspec/openspec-explore/SKILL.md +288 -0
- package/skills/openspec/openspec-explore/_meta.json +1 -0
- package/skills/openspec/openspec-ff-change/SKILL.md +101 -0
- package/skills/openspec/openspec-ff-change/_meta.json +1 -0
- package/skills/openspec/openspec-new-change/SKILL.md +74 -0
- package/skills/openspec/openspec-new-change/_meta.json +1 -0
- package/skills/openspec/openspec-onboard/SKILL.md +554 -0
- package/skills/openspec/openspec-onboard/_meta.json +1 -0
- package/skills/openspec/openspec-sync-specs/SKILL.md +138 -0
- package/skills/openspec/openspec-sync-specs/_meta.json +1 -0
- package/skills/openspec/openspec-verify-change/SKILL.md +168 -0
- package/skills/openspec/openspec-verify-change/_meta.json +1 -0
- package/skills/pr/pdd-multi-review/SKILL.md +534 -0
- package/skills/pr/pdd-multi-review/_meta.json +1 -0
- package/skills/pr/pdd-pr-batch/SKILL.md +303 -0
- package/skills/pr/pdd-pr-batch/_meta.json +1 -0
- package/skills/pr/pdd-pr-create/SKILL.md +344 -0
- package/skills/pr/pdd-pr-create/_meta.json +1 -0
- package/skills/pr/pdd-pr-merge/SKILL.md +286 -0
- package/skills/pr/pdd-pr-merge/_meta.json +1 -0
- package/skills/pr/pdd-pr-review/SKILL.md +217 -0
- package/skills/pr/pdd-pr-review/_meta.json +1 -0
- package/skills/pr/pdd-task-manager/SKILL.md +636 -0
- package/skills/pr/pdd-task-manager/_meta.json +1 -0
- package/skills/pr/pdd-template-engine/SKILL.md +306 -0
- package/skills/pr/pdd-template-engine/_meta.json +1 -0
- package/templates/behavior-shaping/iron-law-template.md +87 -0
- package/templates/behavior-shaping/rationalization-template.md +62 -0
- package/templates/behavior-shaping/red-flags-template.md +70 -0
- package/templates/bilingual-template.md +139 -0
- package/templates/config/default.yaml +47 -0
- package/templates/project/default/README.md +31 -0
- package/templates/project/frontend/README.md +46 -0
- 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
|