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,1170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDD Python SDK 核心客户端
|
|
3
|
+
|
|
4
|
+
提供与 PDD 服务端交互的所有 API,包括规格生成、代码生成、
|
|
5
|
+
功能验证、代码审查等功能。基于 Python 标准库实现异步 HTTP 通信。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.parse import urlencode
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
15
|
+
|
|
16
|
+
# 导入内部模块
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
PDDError,
|
|
19
|
+
ConnectionError as PDDConnectionError,
|
|
20
|
+
AuthError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
ServerError,
|
|
23
|
+
TimeoutError as PDDTimeoutError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
)
|
|
26
|
+
from .models import (
|
|
27
|
+
SpecResult,
|
|
28
|
+
CodeResult,
|
|
29
|
+
VerifyResult,
|
|
30
|
+
ReviewResult,
|
|
31
|
+
SkillInfo,
|
|
32
|
+
Session,
|
|
33
|
+
StatusResult,
|
|
34
|
+
ServerInfo,
|
|
35
|
+
BatchResult,
|
|
36
|
+
FeatureInfo,
|
|
37
|
+
GeneratedFile,
|
|
38
|
+
VerifyIssue,
|
|
39
|
+
VerifyCriterion,
|
|
40
|
+
ReviewFinding,
|
|
41
|
+
Severity,
|
|
42
|
+
TaskStatus,
|
|
43
|
+
EventData,
|
|
44
|
+
)
|
|
45
|
+
from .events import EventEmitter, Events, EventHandler
|
|
46
|
+
from .utils import (
|
|
47
|
+
get_logger,
|
|
48
|
+
format_duration,
|
|
49
|
+
validate_endpoint,
|
|
50
|
+
validate_api_key,
|
|
51
|
+
retry,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 模块级日志器
|
|
55
|
+
logger = get_logger("pdd_sdk.client")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PDDClient:
|
|
59
|
+
"""
|
|
60
|
+
PDD SDK 核心客户端类
|
|
61
|
+
|
|
62
|
+
提供完整的 PDD API 交互能力,包括:
|
|
63
|
+
- 规格生成(从 PRD 提取功能点)
|
|
64
|
+
- 代码生成(根据规格生成实现代码)
|
|
65
|
+
- 功能验证(验证代码是否符合验收标准)
|
|
66
|
+
- 代码审查(静态代码分析)
|
|
67
|
+
- 批量操作(批量生成/验证)
|
|
68
|
+
- 会话管理(开发会话生命周期)
|
|
69
|
+
- 事件系统(请求生命周期监听)
|
|
70
|
+
|
|
71
|
+
所有核心 API 方法都是异步的(async),使用 asyncio 和标准库实现。
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
endpoint: API 服务端地址
|
|
75
|
+
api_key: 认证密钥
|
|
76
|
+
timeout: 请求超时时间(秒)
|
|
77
|
+
debug: 调试模式开关
|
|
78
|
+
max_retries: 最大重试次数
|
|
79
|
+
retry_delay: 重试初始延迟(秒)
|
|
80
|
+
enable_cache: 是否启用内存缓存
|
|
81
|
+
cache_ttl: 缓存有效期(秒)
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> import asyncio
|
|
85
|
+
>>> from pdd_sdk import PDDClient
|
|
86
|
+
>>>
|
|
87
|
+
>>> async def main():
|
|
88
|
+
... client = PDDClient(
|
|
89
|
+
... endpoint="http://localhost:3000",
|
|
90
|
+
... api_key="your-api-key",
|
|
91
|
+
... debug=True
|
|
92
|
+
... )
|
|
93
|
+
...
|
|
94
|
+
... # 监听事件
|
|
95
|
+
... client.on("request:end", lambda e: print(f"完成: {e['path']}"))
|
|
96
|
+
...
|
|
97
|
+
... # 生成规格
|
|
98
|
+
... result = await client.generate_spec(prd_path="./docs/prd.prdx")
|
|
99
|
+
... print(f"提取了 {result.feature_count} 个功能点")
|
|
100
|
+
>>>
|
|
101
|
+
>>> asyncio.run(main())
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# 默认配置常量
|
|
105
|
+
DEFAULT_ENDPOINT = "http://localhost:3000"
|
|
106
|
+
"""默认 API 端点"""
|
|
107
|
+
DEFAULT_TIMEOUT = 30
|
|
108
|
+
"""默认超时时间(秒)"""
|
|
109
|
+
DEFAULT_MAX_RETRIES = 3
|
|
110
|
+
"""默认最大重试次数"""
|
|
111
|
+
DEFAULT_RETRY_DELAY = 1.0
|
|
112
|
+
"""默认重试延迟(秒)"""
|
|
113
|
+
DEFAULT_CACHE_TTL = 300
|
|
114
|
+
"""默认缓存有效期(秒)"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
endpoint: str = DEFAULT_ENDPOINT,
|
|
119
|
+
api_key: str = "",
|
|
120
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
121
|
+
debug: bool = False,
|
|
122
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
123
|
+
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
124
|
+
enable_cache: bool = True,
|
|
125
|
+
cache_ttl: int = DEFAULT_CACHE_TTL,
|
|
126
|
+
):
|
|
127
|
+
"""
|
|
128
|
+
初始化 PDD 客户端
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
endpoint: PDD 服务端 URL 地址(如 http://localhost:3000)
|
|
132
|
+
api_key: API 认证密钥
|
|
133
|
+
timeout: HTTP 请求超时时间(秒),默认 30 秒
|
|
134
|
+
debug: 是否启用调试模式(输出详细日志)
|
|
135
|
+
max_retries: 失败时的最大自动重试次数
|
|
136
|
+
retry_delay: 重试之间的初始延迟时间(秒)
|
|
137
|
+
enable_cache: 是否启用响应缓存
|
|
138
|
+
cache_ttl: 缓存有效期(秒)
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: 如果 endpoint 或 api_key 格式无效
|
|
142
|
+
"""
|
|
143
|
+
# 验证并存储配置
|
|
144
|
+
self.endpoint = validate_endpoint(endpoint)
|
|
145
|
+
self.api_key = validate_api_key(api_key) if api_key else ""
|
|
146
|
+
self.timeout = timeout
|
|
147
|
+
self.debug = debug
|
|
148
|
+
self.max_retries = max_retries
|
|
149
|
+
self.retry_delay = retry_delay
|
|
150
|
+
self.enable_cache = enable_cache
|
|
151
|
+
self.cache_ttl = cache_ttl
|
|
152
|
+
|
|
153
|
+
# 初始化日志级别
|
|
154
|
+
if debug:
|
|
155
|
+
self._logger = get_logger("pdd_sdk.client", level=10) # DEBUG
|
|
156
|
+
else:
|
|
157
|
+
self._logger = logger
|
|
158
|
+
|
|
159
|
+
# 初始化事件发射器
|
|
160
|
+
self._events = EventEmitter()
|
|
161
|
+
|
|
162
|
+
# 内部状态
|
|
163
|
+
self._session_id: Optional[str] = None
|
|
164
|
+
self._request_count = 0
|
|
165
|
+
self._last_request_time: Optional[float] = None
|
|
166
|
+
|
|
167
|
+
self._logger.info(
|
|
168
|
+
f"PDD 客户端初始化完成 | 端点: {self.endpoint} | "
|
|
169
|
+
f"超时: {timeout}s | 重试: {max_retries}次 | 缓存: {'开启' if enable_cache else '关闭'}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ==================== 事件系统代理方法 ====================
|
|
173
|
+
|
|
174
|
+
def on(self, event: str, callback: EventHandler) -> "PDDClient":
|
|
175
|
+
"""
|
|
176
|
+
注册事件监听器
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
event: 事件名称(使用 Events 常量或自定义字符串)
|
|
180
|
+
callback: 回调函数
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
自身,支持链式调用
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> client.on(Events.REQUEST_END, lambda e: print(e))
|
|
187
|
+
"""
|
|
188
|
+
self._events.on(event, callback)
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def off(self, event: str, callback: Optional[EventHandler] = None) -> "PDDClient":
|
|
192
|
+
"""
|
|
193
|
+
移除事件监听器
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
event: 事件名称
|
|
197
|
+
callback: 要移除的回调,None 表示移除所有
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
自身,支持链式调用
|
|
201
|
+
"""
|
|
202
|
+
self._events.off(event, callback)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
async def emit(self, event: str, **data: Any) -> None:
|
|
206
|
+
"""
|
|
207
|
+
异步触发事件
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
event: 事件名称
|
|
211
|
+
**data: 事件负载数据
|
|
212
|
+
"""
|
|
213
|
+
await self._events.async_emit(event, **data)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def events(self) -> EventEmitter:
|
|
217
|
+
"""获取事件发射器实例"""
|
|
218
|
+
return self._events
|
|
219
|
+
|
|
220
|
+
# ==================== 核心 API 方法 ====================
|
|
221
|
+
|
|
222
|
+
async def generate_spec(
|
|
223
|
+
self,
|
|
224
|
+
prd_path: str,
|
|
225
|
+
template: Optional[str] = None,
|
|
226
|
+
output_dir: Optional[str] = None,
|
|
227
|
+
dry_run: bool = False,
|
|
228
|
+
) -> SpecResult:
|
|
229
|
+
"""
|
|
230
|
+
从 PRD 文档生成开发规格
|
|
231
|
+
|
|
232
|
+
解析 PRD 文档,提取功能点矩阵,生成结构化的开发规格文档。
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
prd_path: PRD 文档路径(支持 .prdx, .md, .docx 等格式)
|
|
236
|
+
template: 可选的规格模板名称
|
|
237
|
+
output_dir: 输出目录(可选)
|
|
238
|
+
dry_run: 是否仅预览不实际生成文件
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
SpecResult 包含生成的规格信息、功能点列表等
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValidationError: 如果 PRD 路径无效或格式不支持
|
|
245
|
+
PDDConnectionError: 如果无法连接到服务端
|
|
246
|
+
AuthError: 如果认证失败
|
|
247
|
+
ServerError: 如果服务端处理出错
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> result = await client.generate_spec(
|
|
251
|
+
... prd_path="./requirements/user-system.prdx",
|
|
252
|
+
... template="standard",
|
|
253
|
+
... output_dir="./specs"
|
|
254
|
+
... )
|
|
255
|
+
>>> print(f"成功提取 {result.feature_count} 个功能点")
|
|
256
|
+
"""
|
|
257
|
+
start_time = time.time()
|
|
258
|
+
method = "POST"
|
|
259
|
+
path = "/api/v1/specs/generate"
|
|
260
|
+
|
|
261
|
+
payload = {
|
|
262
|
+
"prd_path": prd_path,
|
|
263
|
+
"dry_run": dry_run,
|
|
264
|
+
}
|
|
265
|
+
if template:
|
|
266
|
+
payload["template"] = template
|
|
267
|
+
if output_dir:
|
|
268
|
+
payload["output_dir"] = output_dir
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
await self._emit_event(Events.REQUEST_START, {
|
|
272
|
+
"method": method, "path": path, "payload": payload
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
response = await self._request(method, path, payload)
|
|
276
|
+
result = self._parse_spec_result(response)
|
|
277
|
+
|
|
278
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
279
|
+
result.duration_ms = duration_ms
|
|
280
|
+
|
|
281
|
+
await self._emit_event(Events.REQUEST_END, {
|
|
282
|
+
"method": method,
|
|
283
|
+
"path": path,
|
|
284
|
+
"duration_ms": duration_ms,
|
|
285
|
+
"success": result.success,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
self._logger.info(
|
|
289
|
+
f"规格生成完成 | 功能点数: {result.feature_count} | "
|
|
290
|
+
f"耗时: {format_duration(duration_ms)}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
297
|
+
await self._emit_event(Events.REQUEST_ERROR, {
|
|
298
|
+
"method": method, "path": path, "error": str(e),
|
|
299
|
+
"duration_ms": duration_ms
|
|
300
|
+
})
|
|
301
|
+
raise
|
|
302
|
+
|
|
303
|
+
async def generate_code(
|
|
304
|
+
self,
|
|
305
|
+
spec_path: str,
|
|
306
|
+
feature_id: Optional[str] = None,
|
|
307
|
+
output_dir: Optional[str] = None,
|
|
308
|
+
dry_run: bool = False,
|
|
309
|
+
) -> CodeResult:
|
|
310
|
+
"""
|
|
311
|
+
根据开发规格生成代码
|
|
312
|
+
|
|
313
|
+
将规格中的功能点转化为可执行的代码实现。
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
spec_path: 开发规格文件路径
|
|
317
|
+
feature_id: 指定要生成的功能点 ID(None 则生成全部)
|
|
318
|
+
output_dir: 代码输出目录
|
|
319
|
+
dry_run: 是否仅预览
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
CodeResult 包含生成的文件列表、代码行数等信息
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ValidationError: 如果规格路径无效
|
|
326
|
+
PDDConnectionError: 连接失败
|
|
327
|
+
ServerError: 服务端错误
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> result = await client.generate_code(
|
|
331
|
+
... spec_path="./specs/user-system.spec.json",
|
|
332
|
+
... feature_id="F001",
|
|
333
|
+
... output_dir="./src"
|
|
334
|
+
... )
|
|
335
|
+
>>> print(f"生成了 {result.file_count} 个文件")
|
|
336
|
+
"""
|
|
337
|
+
start_time = time.time()
|
|
338
|
+
method = "POST"
|
|
339
|
+
path = "/api/v1/code/generate"
|
|
340
|
+
|
|
341
|
+
payload = {
|
|
342
|
+
"spec_path": spec_path,
|
|
343
|
+
"dry_run": dry_run,
|
|
344
|
+
}
|
|
345
|
+
if feature_id:
|
|
346
|
+
payload["feature_id"] = feature_id
|
|
347
|
+
if output_dir:
|
|
348
|
+
payload["output_dir"] = output_dir
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
await self._emit_event(Events.REQUEST_START, {
|
|
352
|
+
"method": method, "path": path, "payload": payload
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
response = await self._request(method, path, payload)
|
|
356
|
+
result = self._parse_code_result(response)
|
|
357
|
+
|
|
358
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
359
|
+
result.duration_ms = duration_ms
|
|
360
|
+
|
|
361
|
+
await self._emit_event(Events.REQUEST_END, {
|
|
362
|
+
"method": method,
|
|
363
|
+
"path": path,
|
|
364
|
+
"duration_ms": duration_ms,
|
|
365
|
+
"success": result.success,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
self._logger.info(
|
|
369
|
+
f"代码生成完成 | 功能点: {result.feature_id} | "
|
|
370
|
+
f"文件数: {result.file_count} | 代码行数: {result.lines_of_code}"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
377
|
+
await self._emit_event(Events.REQUEST_ERROR, {
|
|
378
|
+
"method": method, "path": path, "error": str(e),
|
|
379
|
+
"duration_ms": duration_ms
|
|
380
|
+
})
|
|
381
|
+
raise
|
|
382
|
+
|
|
383
|
+
async def verify_feature(
|
|
384
|
+
self,
|
|
385
|
+
spec_path: str,
|
|
386
|
+
source_dir: str = "./src",
|
|
387
|
+
format: str = "json",
|
|
388
|
+
) -> VerifyResult:
|
|
389
|
+
"""
|
|
390
|
+
验证功能实现是否符合开发规格
|
|
391
|
+
|
|
392
|
+
对比源代码和规格定义的验收标准,检查覆盖率。
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
spec_path: 开发规格文件路径
|
|
396
|
+
source_dir: 源代码目录
|
|
397
|
+
format: 输出格式 (json/table/markdown)
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
VerifyResult 包含覆盖率、通过/未通过的标准列表等
|
|
401
|
+
|
|
402
|
+
Raises:
|
|
403
|
+
ValidationError: 参数验证失败
|
|
404
|
+
PDDConnectionError: 连接失败
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
>>> result = await client.verify_feature(
|
|
408
|
+
... spec_path="./specs/user-system.spec.json",
|
|
409
|
+
... source_dir="./src"
|
|
410
|
+
... )
|
|
411
|
+
>>> print(f"覆盖率: {result.coverage_percent}%")
|
|
412
|
+
"""
|
|
413
|
+
start_time = time.time()
|
|
414
|
+
method = "POST"
|
|
415
|
+
path = "/api/v1/verify"
|
|
416
|
+
|
|
417
|
+
payload = {
|
|
418
|
+
"spec_path": spec_path,
|
|
419
|
+
"source_dir": source_dir,
|
|
420
|
+
"format": format,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
await self._emit_event(Events.REQUEST_START, {
|
|
425
|
+
"method": method, "path": path, "payload": payload
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
response = await self._request(method, path, payload)
|
|
429
|
+
result = self._parse_verify_result(response)
|
|
430
|
+
|
|
431
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
432
|
+
result.duration_ms = duration_ms
|
|
433
|
+
|
|
434
|
+
await self._emit_event(Events.REQUEST_END, {
|
|
435
|
+
"method": method,
|
|
436
|
+
"path": path,
|
|
437
|
+
"duration_ms": duration_ms,
|
|
438
|
+
"success": result.success,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
self._logger.info(
|
|
442
|
+
f"功能验证完成 | 覆盖率: {result.coverage_percent}% | "
|
|
443
|
+
f"通过: {len(result.criteria_passed)} | 未通过: {len(result.criteria_failed)}"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
450
|
+
await self._emit_event(Events.REQUEST_ERROR, {
|
|
451
|
+
"method": method, "path": path, "error": str(e),
|
|
452
|
+
"duration_ms": duration_ms
|
|
453
|
+
})
|
|
454
|
+
raise
|
|
455
|
+
|
|
456
|
+
async def code_review(
|
|
457
|
+
self,
|
|
458
|
+
source_dir: str = "./src",
|
|
459
|
+
rules: Optional[List[str]] = None,
|
|
460
|
+
format: str = "table",
|
|
461
|
+
) -> ReviewResult:
|
|
462
|
+
"""
|
|
463
|
+
执行代码审查
|
|
464
|
+
|
|
465
|
+
对源代码进行静态分析和质量评估。
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
source_dir: 待审查的源代码目录
|
|
469
|
+
rules: 指定应用的规则集(None 使用默认规则)
|
|
470
|
+
format: 输出格式 (json/table/markdown)
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
ReviewResult 包含评分、等级、问题列表等
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
ValidationError: 参数验证失败
|
|
477
|
+
PDDConnectionError: 连接失败
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
>>> result = await client.code_review(source_dir="./src")
|
|
481
|
+
>>> print(f"评分: {result.score}/100 | 等级: {result.grade}")
|
|
482
|
+
"""
|
|
483
|
+
start_time = time.time()
|
|
484
|
+
method = "POST"
|
|
485
|
+
path = "/api/v1/review"
|
|
486
|
+
|
|
487
|
+
payload = {"source_dir": source_dir, "format": format}
|
|
488
|
+
if rules:
|
|
489
|
+
payload["rules"] = rules
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
await self._emit_event(Events.REQUEST_START, {
|
|
493
|
+
"method": method, "path": path, "payload": payload
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
response = await self._request(method, path, payload)
|
|
497
|
+
result = self._parse_review_result(response)
|
|
498
|
+
|
|
499
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
500
|
+
result.duration_ms = duration_ms
|
|
501
|
+
|
|
502
|
+
await self._emit_event(Events.REQUEST_END, {
|
|
503
|
+
"method": method,
|
|
504
|
+
"path": path,
|
|
505
|
+
"duration_ms": duration_ms,
|
|
506
|
+
"success": result.success,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
self._logger.info(
|
|
510
|
+
f"代码审查完成 | 评分: {result.score}/100 | "
|
|
511
|
+
f"等级: {result.grade} | 问题数: {result.finding_count}"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return result
|
|
515
|
+
|
|
516
|
+
except Exception as e:
|
|
517
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
518
|
+
await self._emit_event(Events.REQUEST_ERROR, {
|
|
519
|
+
"method": method, "path": path, "error": str(e),
|
|
520
|
+
"duration_ms": duration_ms
|
|
521
|
+
})
|
|
522
|
+
raise
|
|
523
|
+
|
|
524
|
+
async def list_skills(
|
|
525
|
+
self,
|
|
526
|
+
category: Optional[str] = None,
|
|
527
|
+
language: Optional[str] = None,
|
|
528
|
+
) -> List[SkillInfo]:
|
|
529
|
+
"""
|
|
530
|
+
获取可用技能列表
|
|
531
|
+
|
|
532
|
+
查询当前 PDD 实例中注册的所有技能信息。
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
category: 技能分类过滤(可选)
|
|
536
|
+
language: 编程语言过滤(可选)
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
技能信息列表
|
|
540
|
+
|
|
541
|
+
Example:
|
|
542
|
+
>>> skills = await client.list_skills(category="analysis")
|
|
543
|
+
>>> for skill in skills:
|
|
544
|
+
... print(f"{skill.name}: {skill.description}")
|
|
545
|
+
"""
|
|
546
|
+
params: Dict[str, str] = {}
|
|
547
|
+
if category:
|
|
548
|
+
params["category"] = category
|
|
549
|
+
if language:
|
|
550
|
+
params["language"] = language
|
|
551
|
+
|
|
552
|
+
query_string = urlencode(params) if params else ""
|
|
553
|
+
path = f"/api/v1/skills{('?'+query_string) if query_string else ''}"
|
|
554
|
+
|
|
555
|
+
response = await self._request("GET", path)
|
|
556
|
+
return self._parse_skills_list(response)
|
|
557
|
+
|
|
558
|
+
async def get_status(self) -> StatusResult:
|
|
559
|
+
"""
|
|
560
|
+
获取服务状态
|
|
561
|
+
|
|
562
|
+
检查 PDD 服务端的健康状态和运行信息。
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
StatusResult 包含健康状态、版本信息等
|
|
566
|
+
|
|
567
|
+
Example:
|
|
568
|
+
>>> status = await client.get_status()
|
|
569
|
+
>>> print(f"服务{'正常' if status.healthy else '异常'}")
|
|
570
|
+
"""
|
|
571
|
+
start_time = time.time()
|
|
572
|
+
path = "/api/v1/status"
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
response = await self._request("GET", path)
|
|
576
|
+
server_info = self._parse_server_info(response.get("server", {}))
|
|
577
|
+
|
|
578
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
579
|
+
|
|
580
|
+
return StatusResult(
|
|
581
|
+
healthy=response.get("healthy", False),
|
|
582
|
+
server_info=server_info,
|
|
583
|
+
response_time_ms=duration_ms,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
except Exception:
|
|
587
|
+
return StatusResult(
|
|
588
|
+
healthy=False,
|
|
589
|
+
response_time_ms=int((time.time() - start_time) * 1000),
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# ==================== 批量操作 ====================
|
|
593
|
+
|
|
594
|
+
async def batch_generate_specs(
|
|
595
|
+
self, specs_list: List[Dict[str, Any]]
|
|
596
|
+
) -> List[SpecResult]:
|
|
597
|
+
"""
|
|
598
|
+
批量生成多个规格
|
|
599
|
+
|
|
600
|
+
并发执行多个规格生成任务。
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
specs_list: 规格参数列表,每个元素包含 generate_spec 所需参数
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
规格结果列表,顺序与输入一致
|
|
607
|
+
|
|
608
|
+
Example:
|
|
609
|
+
>>> specs = [
|
|
610
|
+
... {"prd_path": "./prd1.prdx"},
|
|
611
|
+
... {"prd_path": "./prd2.prdx"},
|
|
612
|
+
... ]
|
|
613
|
+
>>> results = await client.batch_generate_specs(specs)
|
|
614
|
+
"""
|
|
615
|
+
total_start = time.time()
|
|
616
|
+
await self._emit_event(Events.BATCH_START, {
|
|
617
|
+
"total": len(specs_list), "type": "generate_spec"
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
tasks = [self.generate_spec(**params) for params in specs_list]
|
|
621
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
622
|
+
|
|
623
|
+
processed_results = []
|
|
624
|
+
for i, result in enumerate(results):
|
|
625
|
+
if isinstance(result, Exception):
|
|
626
|
+
processed_results.append(SpecResult(
|
|
627
|
+
success=False,
|
|
628
|
+
spec_id="",
|
|
629
|
+
spec_path="",
|
|
630
|
+
errors=[str(result)]
|
|
631
|
+
))
|
|
632
|
+
else:
|
|
633
|
+
processed_results.append(result)
|
|
634
|
+
|
|
635
|
+
await self._emit_event(Events.BATCH_PROGRESS, {
|
|
636
|
+
"current": i + 1,
|
|
637
|
+
"total": len(specs_list),
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
total_duration = int((time.time() - total_start) * 1000)
|
|
641
|
+
await self._emit_event(Events.BATCH_COMPLETE, {
|
|
642
|
+
"total": len(specs_list),
|
|
643
|
+
"duration_ms": total_duration,
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
return processed_results
|
|
647
|
+
|
|
648
|
+
async def batch_verify(self, features_list: List[Dict[str, Any]]) -> List[VerifyResult]:
|
|
649
|
+
"""
|
|
650
|
+
批量验证多个功能
|
|
651
|
+
|
|
652
|
+
并发执行多个功能验证任务。
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
features_list: 验证参数列表
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
验证结果列表
|
|
659
|
+
"""
|
|
660
|
+
tasks = [
|
|
661
|
+
self.verify_feature(**params) for params in features_list
|
|
662
|
+
]
|
|
663
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
664
|
+
|
|
665
|
+
processed_results = []
|
|
666
|
+
for result in results:
|
|
667
|
+
if isinstance(result, Exception):
|
|
668
|
+
processed_results.append(VerifyResult(
|
|
669
|
+
success=False,
|
|
670
|
+
errors=[str(result)]
|
|
671
|
+
))
|
|
672
|
+
else:
|
|
673
|
+
processed_results.append(result)
|
|
674
|
+
|
|
675
|
+
return processed_results
|
|
676
|
+
|
|
677
|
+
# ==================== 会话管理 ====================
|
|
678
|
+
|
|
679
|
+
async def create_session(self, name: Optional[str] = None) -> Session:
|
|
680
|
+
"""
|
|
681
|
+
创建新的开发会话
|
|
682
|
+
|
|
683
|
+
会话用于组织相关的规格和代码生成任务。
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
name: 会话名称(可选,自动生成唯一名称)
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
新创建的会话对象
|
|
690
|
+
|
|
691
|
+
Example:
|
|
692
|
+
>>> session = await client.create_session(name="用户模块开发")
|
|
693
|
+
>>> print(f"会话ID: {session.session_id}")
|
|
694
|
+
"""
|
|
695
|
+
payload: Dict[str, Any] = {}
|
|
696
|
+
if name:
|
|
697
|
+
payload["name"] = name
|
|
698
|
+
|
|
699
|
+
response = await self._request("POST", "/api/v1/sessions", payload)
|
|
700
|
+
session_data = response.get("session", {})
|
|
701
|
+
session = self._parse_session(session_data)
|
|
702
|
+
|
|
703
|
+
await self._emit_event(Events.SESSION_CREATED, {
|
|
704
|
+
"session_id": session.session_id,
|
|
705
|
+
"name": session.name,
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
self._session_id = session.session_id
|
|
709
|
+
return session
|
|
710
|
+
|
|
711
|
+
async def get_session(self, session_id: str) -> Session:
|
|
712
|
+
"""
|
|
713
|
+
获取会话详情
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
session_id: 会话 ID
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
会话对象
|
|
720
|
+
|
|
721
|
+
Raises:
|
|
722
|
+
PDDError: 如果会话不存在
|
|
723
|
+
"""
|
|
724
|
+
response = await self._request("GET", f"/api/v1/sessions/{session_id}")
|
|
725
|
+
return self._parse_session(response.get("session", {}))
|
|
726
|
+
|
|
727
|
+
async def list_sessions(self) -> List[Session]:
|
|
728
|
+
"""
|
|
729
|
+
列出所有会话
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
会话列表
|
|
733
|
+
"""
|
|
734
|
+
response = await self._request("GET", "/api/v1/sessions")
|
|
735
|
+
sessions_data = response.get("sessions", [])
|
|
736
|
+
return [self._parse_session(s) for s in sessions_data]
|
|
737
|
+
|
|
738
|
+
# ==================== 工具方法 ====================
|
|
739
|
+
|
|
740
|
+
def health_check(self) -> bool:
|
|
741
|
+
"""
|
|
742
|
+
同步健康检查
|
|
743
|
+
|
|
744
|
+
快速检测服务端是否可达。此方法是同步的,适合在非异步上下文中使用。
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
True 如果服务端健康,False 否则
|
|
748
|
+
|
|
749
|
+
Example:
|
|
750
|
+
>>> if client.health_check():
|
|
751
|
+
... print("服务可用")
|
|
752
|
+
"""
|
|
753
|
+
try:
|
|
754
|
+
url = f"{self.endpoint}/health"
|
|
755
|
+
req = Request(url, method="GET")
|
|
756
|
+
|
|
757
|
+
with urlopen(req, timeout=min(self.timeout, 5)) as resp:
|
|
758
|
+
data = json.loads(resp.read().decode())
|
|
759
|
+
return data.get("status") == "ok"
|
|
760
|
+
except Exception as e:
|
|
761
|
+
self._logger.debug(f"健康检查失败: {e}")
|
|
762
|
+
return False
|
|
763
|
+
|
|
764
|
+
async def async_health_check(self) -> bool:
|
|
765
|
+
"""
|
|
766
|
+
异步健康检查
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
True 如果服务端健康
|
|
770
|
+
"""
|
|
771
|
+
loop = asyncio.get_event_loop()
|
|
772
|
+
return await loop.run_in_executor(None, self.health_check)
|
|
773
|
+
|
|
774
|
+
def get_server_info(self) -> Optional[ServerInfo]:
|
|
775
|
+
"""
|
|
776
|
+
同步获取服务器信息
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
ServerInfo 或 None(如果无法连接)
|
|
780
|
+
"""
|
|
781
|
+
try:
|
|
782
|
+
url = f"{self.endpoint}/api/v1/status"
|
|
783
|
+
req = Request(url, method="GET")
|
|
784
|
+
self._add_headers(req)
|
|
785
|
+
|
|
786
|
+
with urlopen(req, timeout=self.timeout) as resp:
|
|
787
|
+
data = json.loads(resp.read().decode())
|
|
788
|
+
return self._parse_server_info(data.get("server", {}))
|
|
789
|
+
except Exception as e:
|
|
790
|
+
self._logger.debug(f"获取服务器信息失败: {e}")
|
|
791
|
+
return None
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def request_stats(self) -> Dict[str, Any]:
|
|
795
|
+
"""
|
|
796
|
+
获取请求统计信息
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
包含请求数、最后请求时间等的字典
|
|
800
|
+
"""
|
|
801
|
+
return {
|
|
802
|
+
"total_requests": self._request_count,
|
|
803
|
+
"last_request_time": self._last_request_time,
|
|
804
|
+
"endpoint": self.endpoint,
|
|
805
|
+
"has_active_session": self._session_id is not None,
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async def close(self) -> None:
|
|
809
|
+
"""
|
|
810
|
+
关闭客户端,释放资源
|
|
811
|
+
|
|
812
|
+
清理缓存、移除所有事件监听器。
|
|
813
|
+
通常在程序退出前调用。
|
|
814
|
+
"""
|
|
815
|
+
self._events.remove_all_listeners()
|
|
816
|
+
self._logger.info("客户端已关闭")
|
|
817
|
+
|
|
818
|
+
# ==================== 内部方法 ====================
|
|
819
|
+
|
|
820
|
+
async def _request(
|
|
821
|
+
self,
|
|
822
|
+
method: str,
|
|
823
|
+
path: str,
|
|
824
|
+
data: Optional[Dict[str, Any]] = None,
|
|
825
|
+
) -> Dict[str, Any]:
|
|
826
|
+
"""
|
|
827
|
+
发送 HTTP 请求(内部方法)
|
|
828
|
+
|
|
829
|
+
使用线程池执行同步 HTTP 请求,模拟异步行为。
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
method: HTTP 方法 (GET/POST/PUT/DELETE)
|
|
833
|
+
path: API 路径
|
|
834
|
+
data: 请求数据(仅 POST/PUT 时使用)
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
解析后的 JSON 响应数据
|
|
838
|
+
|
|
839
|
+
Raises:
|
|
840
|
+
PDDConnectionError: 连接失败
|
|
841
|
+
AuthError: 认证失败 (401)
|
|
842
|
+
ValidationError: 参数错误 (400)
|
|
843
|
+
ServerError: 服务端错误 (5xx)
|
|
844
|
+
PDDTimeoutError: 超时
|
|
845
|
+
RateLimitError: 限流 (429)
|
|
846
|
+
"""
|
|
847
|
+
self._request_count += 1
|
|
848
|
+
self._last_request_time = time.time()
|
|
849
|
+
|
|
850
|
+
# 在线程池中执行同步 HTTP 请求
|
|
851
|
+
loop = asyncio.get_event_loop()
|
|
852
|
+
response = await loop.run_in_executor(
|
|
853
|
+
None,
|
|
854
|
+
lambda: self._sync_request(method, path, data)
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
return response
|
|
858
|
+
|
|
859
|
+
def _sync_request(
|
|
860
|
+
self,
|
|
861
|
+
method: str,
|
|
862
|
+
path: str,
|
|
863
|
+
data: Optional[Dict[str, Any]] = None,
|
|
864
|
+
) -> Dict[str, Any]:
|
|
865
|
+
"""
|
|
866
|
+
同步 HTTP 请求实现(内部方法)
|
|
867
|
+
|
|
868
|
+
使用标准库 urllib 实现 HTTP 通信。
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
method: HTTP 方法
|
|
872
|
+
path: API 路径
|
|
873
|
+
data: 请求数据
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
JSON 响应数据
|
|
877
|
+
"""
|
|
878
|
+
url = f"{self.endpoint}{path}"
|
|
879
|
+
body = None
|
|
880
|
+
|
|
881
|
+
if data and method in ("POST", "PUT"):
|
|
882
|
+
body = json.dumps(data).encode("utf-8")
|
|
883
|
+
|
|
884
|
+
req = Request(url, data=body, method=method)
|
|
885
|
+
self._add_headers(req)
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
self._logger.debug(f"发送请求: {method} {url}")
|
|
889
|
+
|
|
890
|
+
with urlopen(req, timeout=self.timeout) as resp:
|
|
891
|
+
response_data = resp.read()
|
|
892
|
+
status_code = resp.status if hasattr(resp, 'status') else 200
|
|
893
|
+
|
|
894
|
+
self._logger.debug(
|
|
895
|
+
f"收到响应: {status_code} | 大小: {len(response_data)} bytes"
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
result = json.loads(response_data.decode())
|
|
899
|
+
|
|
900
|
+
# 检查业务层错误
|
|
901
|
+
if not result.get("success", True):
|
|
902
|
+
error_msg = result.get("message", "未知错误")
|
|
903
|
+
error_code = result.get("code")
|
|
904
|
+
raise self._map_error(error_msg, error_code, status_code)
|
|
905
|
+
|
|
906
|
+
return result
|
|
907
|
+
|
|
908
|
+
except HTTPError as e:
|
|
909
|
+
error_body = {}
|
|
910
|
+
try:
|
|
911
|
+
error_body = json.loads(e.read().decode())
|
|
912
|
+
except Exception:
|
|
913
|
+
pass
|
|
914
|
+
|
|
915
|
+
status_code = e.code
|
|
916
|
+
raise self._map_error(
|
|
917
|
+
error_body.get("message", str(e)),
|
|
918
|
+
error_body.get("code"),
|
|
919
|
+
status_code
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
except URLError as e:
|
|
923
|
+
raise PDDConnectionError(
|
|
924
|
+
message=f"无法连接到服务端: {e.reason}",
|
|
925
|
+
details={"endpoint": self.endpoint, "reason": str(e.reason)}
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
except TimeoutError as e:
|
|
929
|
+
raise PDDTimeoutError(
|
|
930
|
+
message=f"请求超时 ({self.timeout}s)",
|
|
931
|
+
details={"timeout": self.timeout}
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
def _add_headers(self, req: Request) -> None:
|
|
935
|
+
"""
|
|
936
|
+
添加 HTTP 请求头
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
req: urllib Request 对象
|
|
940
|
+
"""
|
|
941
|
+
req.add_header("Content-Type", "application/json; charset=utf-8")
|
|
942
|
+
req.add_header("Accept", "application/json")
|
|
943
|
+
req.add_header("User-Agent", "PDD-Python-SDK/1.0.0")
|
|
944
|
+
|
|
945
|
+
if self.api_key:
|
|
946
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
947
|
+
|
|
948
|
+
if self._session_id:
|
|
949
|
+
req.add_header("X-Session-ID", self._session_id)
|
|
950
|
+
|
|
951
|
+
def _map_error(
|
|
952
|
+
self,
|
|
953
|
+
message: str,
|
|
954
|
+
code: Optional[str],
|
|
955
|
+
status_code: int,
|
|
956
|
+
) -> PDDError:
|
|
957
|
+
"""
|
|
958
|
+
将 HTTP 错误映射为 SDK 异常
|
|
959
|
+
|
|
960
|
+
Args:
|
|
961
|
+
message: 错误消息
|
|
962
|
+
code: 错误代码
|
|
963
|
+
status_code: HTTP 状态码
|
|
964
|
+
|
|
965
|
+
Returns:
|
|
966
|
+
对应类型的 PDDError 子类
|
|
967
|
+
"""
|
|
968
|
+
if status_code == 401 or code == "AUTH_ERROR":
|
|
969
|
+
return AuthError(message=message, details={"status_code": status_code})
|
|
970
|
+
elif status_code == 400 or code == "VALIDATION_ERROR":
|
|
971
|
+
return ValidationError(message=message, details={"status_code": status_code})
|
|
972
|
+
elif status_code == 429 or code == "RATE_LIMIT_ERROR":
|
|
973
|
+
return RateLimitError(message=message, details={"status_code": status_code})
|
|
974
|
+
elif status_code >= 500:
|
|
975
|
+
return ServerError(message=message, details={"status_code": status_code})
|
|
976
|
+
else:
|
|
977
|
+
return PDDError(message=message, code=code, details={"status_code": status_code})
|
|
978
|
+
|
|
979
|
+
async def _emit_event(self, event_name: str, data: Dict[str, Any]) -> None:
|
|
980
|
+
"""内部方法:触发事件"""
|
|
981
|
+
try:
|
|
982
|
+
await self._events.async_emit(event_name, **data)
|
|
983
|
+
except Exception as e:
|
|
984
|
+
self._logger.debug(f"事件触发失败 ({event_name}): {e}")
|
|
985
|
+
|
|
986
|
+
# ==================== 响应解析方法 ====================
|
|
987
|
+
|
|
988
|
+
@staticmethod
|
|
989
|
+
def _parse_spec_result(data: Dict[str, Any]) -> SpecResult:
|
|
990
|
+
"""解析规格生成响应"""
|
|
991
|
+
raw_features = data.get("features", [])
|
|
992
|
+
features = []
|
|
993
|
+
for f in raw_features:
|
|
994
|
+
if isinstance(f, dict):
|
|
995
|
+
features.append({
|
|
996
|
+
"id": f.get("id", ""),
|
|
997
|
+
"name": f.get("name", ""),
|
|
998
|
+
"description": f.get("description", ""),
|
|
999
|
+
})
|
|
1000
|
+
elif isinstance(f, str):
|
|
1001
|
+
features.append({"id": f, "name": f})
|
|
1002
|
+
|
|
1003
|
+
return SpecResult(
|
|
1004
|
+
success=data.get("success", False),
|
|
1005
|
+
spec_id=data.get("spec_id", ""),
|
|
1006
|
+
spec_path=data.get("spec_path", ""),
|
|
1007
|
+
features=features,
|
|
1008
|
+
warnings=data.get("warnings", []),
|
|
1009
|
+
errors=data.get("errors", []),
|
|
1010
|
+
metadata=data.get("metadata", {}),
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
@staticmethod
|
|
1014
|
+
def _parse_code_result(data: Dict[str, Any]) -> CodeResult:
|
|
1015
|
+
"""解析代码生成响应"""
|
|
1016
|
+
raw_files = data.get("files_generated", [])
|
|
1017
|
+
files = []
|
|
1018
|
+
for f in raw_files:
|
|
1019
|
+
if isinstance(f, dict):
|
|
1020
|
+
files.append(GeneratedFile(
|
|
1021
|
+
path=f.get("path", ""),
|
|
1022
|
+
absolute_path=f.get("absolute_path", ""),
|
|
1023
|
+
lines_of_code=f.get("lines_of_code", 0),
|
|
1024
|
+
language=f.get("language", ""),
|
|
1025
|
+
size_bytes=f.get("size_bytes", 0),
|
|
1026
|
+
))
|
|
1027
|
+
elif isinstance(f, str):
|
|
1028
|
+
files.append(GeneratedFile(path=f))
|
|
1029
|
+
|
|
1030
|
+
return CodeResult(
|
|
1031
|
+
success=data.get("success", False),
|
|
1032
|
+
feature_id=data.get("feature_id", ""),
|
|
1033
|
+
files_generated=files,
|
|
1034
|
+
lines_of_code=data.get("lines_of_code", 0),
|
|
1035
|
+
warnings=data.get("warnings", []),
|
|
1036
|
+
errors=data.get("errors", []),
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
@staticmethod
|
|
1040
|
+
def _parse_verify_result(data: Dict[str, Any]) -> VerifyResult:
|
|
1041
|
+
"""解析验证响应"""
|
|
1042
|
+
raw_issues = data.get("issues", [])
|
|
1043
|
+
issues = []
|
|
1044
|
+
for issue in raw_issues:
|
|
1045
|
+
severity_val = issue.get("severity", "error")
|
|
1046
|
+
severity = next(
|
|
1047
|
+
(s for s in Severity if s.value == severity_val), Severity.ERROR
|
|
1048
|
+
)
|
|
1049
|
+
issues.append(VerifyIssue(
|
|
1050
|
+
file_path=issue.get("file_path", ""),
|
|
1051
|
+
message=issue.get("message", ""),
|
|
1052
|
+
severity=severity,
|
|
1053
|
+
line_number=issue.get("line_number"),
|
|
1054
|
+
suggestion=issue.get("suggestion", ""),
|
|
1055
|
+
))
|
|
1056
|
+
|
|
1057
|
+
return VerifyResult(
|
|
1058
|
+
success=data.get("success", False),
|
|
1059
|
+
coverage_percent=float(data.get("coverage_percent", 0)),
|
|
1060
|
+
criteria_passed=data.get("criteria_passed", []),
|
|
1061
|
+
criteria_failed=data.get("criteria_failed", []),
|
|
1062
|
+
issues=issues,
|
|
1063
|
+
summary=data.get("summary", ""),
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
@staticmethod
|
|
1067
|
+
def _parse_review_result(data: Dict[str, Any]) -> ReviewResult:
|
|
1068
|
+
"""解析代码审查响应"""
|
|
1069
|
+
raw_findings = data.get("findings", [])
|
|
1070
|
+
findings = []
|
|
1071
|
+
for f in raw_findings:
|
|
1072
|
+
severity_val = f.get("severity", "error")
|
|
1073
|
+
severity = next(
|
|
1074
|
+
(s for s in Severity if s.value == severity_val), Severity.ERROR
|
|
1075
|
+
)
|
|
1076
|
+
findings.append(ReviewFinding(
|
|
1077
|
+
rule_id=f.get("rule_id", ""),
|
|
1078
|
+
category=f.get("category", "general"),
|
|
1079
|
+
severity=severity,
|
|
1080
|
+
message=f.get("message", ""),
|
|
1081
|
+
file_path=f.get("file_path", ""),
|
|
1082
|
+
line_number=f.get("line_number"),
|
|
1083
|
+
suggestion=f.get("suggestion", ""),
|
|
1084
|
+
))
|
|
1085
|
+
|
|
1086
|
+
return ReviewResult(
|
|
1087
|
+
success=data.get("success", False),
|
|
1088
|
+
score=float(data.get("score", 0)),
|
|
1089
|
+
grade=data.get("grade", ""),
|
|
1090
|
+
findings=findings,
|
|
1091
|
+
metrics=data.get("metrics", {}),
|
|
1092
|
+
summary=data.get("summary", ""),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
@staticmethod
|
|
1096
|
+
def _parse_skills_list(data: Union[List, Dict]) -> List[SkillInfo]:
|
|
1097
|
+
"""解析技能列表响应"""
|
|
1098
|
+
items = data if isinstance(data, list) else data.get("skills", [])
|
|
1099
|
+
skills = []
|
|
1100
|
+
for item in items:
|
|
1101
|
+
skills.append(SkillInfo(
|
|
1102
|
+
name=item.get("name", ""),
|
|
1103
|
+
version=item.get("version", "1.0.0"),
|
|
1104
|
+
description=item.get("description", ""),
|
|
1105
|
+
category=item.get("category", "general"),
|
|
1106
|
+
triggers=item.get("triggers", []),
|
|
1107
|
+
has_evals=item.get("has_evals", False),
|
|
1108
|
+
author=item.get("author"),
|
|
1109
|
+
tags=item.get("tags", []),
|
|
1110
|
+
))
|
|
1111
|
+
return skills
|
|
1112
|
+
|
|
1113
|
+
@staticmethod
|
|
1114
|
+
def _parse_server_info(data: Dict[str, Any]) -> ServerInfo:
|
|
1115
|
+
"""解析服务器信息"""
|
|
1116
|
+
return ServerInfo(
|
|
1117
|
+
version=data.get("version", "unknown"),
|
|
1118
|
+
uptime=float(data.get("uptime", 0)),
|
|
1119
|
+
supported_apis=data.get("supported_apis", []),
|
|
1120
|
+
available_skills=int(data.get("available_skills", 0)),
|
|
1121
|
+
system_info=data.get("system_info", {}),
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
@staticmethod
|
|
1125
|
+
def _parse_session(data: Dict[str, Any]) -> Session:
|
|
1126
|
+
"""解析会话数据"""
|
|
1127
|
+
from datetime import datetime
|
|
1128
|
+
|
|
1129
|
+
created_at_str = data.get("created_at")
|
|
1130
|
+
updated_at_str = data.get("updated_at")
|
|
1131
|
+
|
|
1132
|
+
try:
|
|
1133
|
+
created_at = datetime.fromisoformat(created_at_str) if created_at_str else datetime.now()
|
|
1134
|
+
except (ValueError, TypeError):
|
|
1135
|
+
created_at = datetime.now()
|
|
1136
|
+
|
|
1137
|
+
try:
|
|
1138
|
+
updated_at = datetime.fromisoformat(updated_at_str) if updated_at_str else datetime.now()
|
|
1139
|
+
except (ValueError, TypeError):
|
|
1140
|
+
updated_at = datetime.now()
|
|
1141
|
+
|
|
1142
|
+
status_val = data.get("status", "pending")
|
|
1143
|
+
status = next(
|
|
1144
|
+
(s for s in TaskStatus if s.value == status_val), TaskStatus.PENDING
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
return Session(
|
|
1148
|
+
session_id=data.get("session_id", ""),
|
|
1149
|
+
name=data.get("name", ""),
|
|
1150
|
+
created_at=created_at,
|
|
1151
|
+
updated_at=updated_at,
|
|
1152
|
+
status=status,
|
|
1153
|
+
specs=data.get("specs", []),
|
|
1154
|
+
metadata=data.get("metadata", {}),
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
def __repr__(self) -> str:
|
|
1158
|
+
"""返回对象的字符串表示"""
|
|
1159
|
+
return (
|
|
1160
|
+
f"PDDClient(endpoint='{self.endpoint}', "
|
|
1161
|
+
f"debug={self.debug}, retries={self.max_retries})"
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
async def __aenter__(self) -> "PDDClient":
|
|
1165
|
+
"""异步上下文管理器入口"""
|
|
1166
|
+
return self
|
|
1167
|
+
|
|
1168
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1169
|
+
"""异步上下文管理器出口"""
|
|
1170
|
+
await self.close()
|