@huajiwuyan/hello 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 +68 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +448 -0
- package/package.json +38 -0
- package/templates/claude/commands/hello.md +760 -0
- package/templates/claude/skills/SKILL.md +90 -0
- package/templates/claude/skills/SKILL.toml +7 -0
- package/templates/claude/skills/assets/icon-large.svg +12 -0
- package/templates/claude/skills/assets/icon-small-400px.svg +12 -0
- package/templates/claude/skills/assets/templates/CHANGELOG.md +24 -0
- package/templates/claude/skills/assets/templates/CHANGELOG_{YYYY}.md +25 -0
- package/templates/claude/skills/assets/templates/INDEX.md +36 -0
- package/templates/claude/skills/assets/templates/archive/_index.md +22 -0
- package/templates/claude/skills/assets/templates/context.md +82 -0
- package/templates/claude/skills/assets/templates/modules/_index.md +22 -0
- package/templates/claude/skills/assets/templates/modules/module.md +35 -0
- package/templates/claude/skills/assets/templates/plan/proposal.md +104 -0
- package/templates/claude/skills/assets/templates/plan/tasks.md +49 -0
- package/templates/claude/skills/references/functions/auto.md +217 -0
- package/templates/claude/skills/references/functions/clean.md +167 -0
- package/templates/claude/skills/references/functions/commit.md +374 -0
- package/templates/claude/skills/references/functions/exec.md +178 -0
- package/templates/claude/skills/references/functions/help.md +105 -0
- package/templates/claude/skills/references/functions/init.md +228 -0
- package/templates/claude/skills/references/functions/plan.md +219 -0
- package/templates/claude/skills/references/functions/review.md +146 -0
- package/templates/claude/skills/references/functions/rollback.md +208 -0
- package/templates/claude/skills/references/functions/test.md +153 -0
- package/templates/claude/skills/references/functions/upgrade.md +371 -0
- package/templates/claude/skills/references/functions/validate.md +147 -0
- package/templates/claude/skills/references/rules/package.md +212 -0
- package/templates/claude/skills/references/rules/scaling.md +150 -0
- package/templates/claude/skills/references/rules/state.md +318 -0
- package/templates/claude/skills/references/rules/tools.md +371 -0
- package/templates/claude/skills/references/services/knowledge.md +408 -0
- package/templates/claude/skills/references/services/templates.md +344 -0
- package/templates/claude/skills/references/stages/analyze.md +201 -0
- package/templates/claude/skills/references/stages/design.md +379 -0
- package/templates/claude/skills/references/stages/develop.md +497 -0
- package/templates/claude/skills/references/stages/evaluate.md +286 -0
- package/templates/claude/skills/references/stages/tweak.md +244 -0
- package/templates/claude/skills/scripts/create_package.py +260 -0
- package/templates/claude/skills/scripts/list_packages.py +145 -0
- package/templates/claude/skills/scripts/migrate_package.py +399 -0
- package/templates/claude/skills/scripts/project_stats.py +438 -0
- package/templates/claude/skills/scripts/upgradewiki.py +321 -0
- package/templates/claude/skills/scripts/utils.py +596 -0
- package/templates/claude/skills/scripts/validate_package.py +309 -0
- package/templates/codex/prompts/hello.md +757 -0
- package/templates/codex/skills/SKILL.md +74 -0
- package/templates/codex/skills/SKILL.toml +7 -0
- package/templates/codex/skills/assets/icon-large.svg +12 -0
- package/templates/codex/skills/assets/icon-small-400px.svg +12 -0
- package/templates/codex/skills/assets/templates/CHANGELOG.md +24 -0
- package/templates/codex/skills/assets/templates/CHANGELOG_{YYYY}.md +25 -0
- package/templates/codex/skills/assets/templates/INDEX.md +36 -0
- package/templates/codex/skills/assets/templates/archive/_index.md +22 -0
- package/templates/codex/skills/assets/templates/context.md +82 -0
- package/templates/codex/skills/assets/templates/modules/_index.md +22 -0
- package/templates/codex/skills/assets/templates/modules/module.md +35 -0
- package/templates/codex/skills/assets/templates/plan/proposal.md +104 -0
- package/templates/codex/skills/assets/templates/plan/tasks.md +29 -0
- package/templates/codex/skills/references/functions/auto.md +181 -0
- package/templates/codex/skills/references/functions/brain.md +275 -0
- package/templates/codex/skills/references/functions/clean.md +154 -0
- package/templates/codex/skills/references/functions/commit.md +265 -0
- package/templates/codex/skills/references/functions/debug/condition-based-waiting.md +151 -0
- package/templates/codex/skills/references/functions/debug/defense-in-depth.md +147 -0
- package/templates/codex/skills/references/functions/debug/root-cause-tracing.md +168 -0
- package/templates/codex/skills/references/functions/debug.md +389 -0
- package/templates/codex/skills/references/functions/exec.md +153 -0
- package/templates/codex/skills/references/functions/help.md +101 -0
- package/templates/codex/skills/references/functions/init.md +221 -0
- package/templates/codex/skills/references/functions/plan.md +178 -0
- package/templates/codex/skills/references/functions/review.md +135 -0
- package/templates/codex/skills/references/functions/rlm.md +864 -0
- package/templates/codex/skills/references/functions/rollback.md +190 -0
- package/templates/codex/skills/references/functions/test.md +140 -0
- package/templates/codex/skills/references/functions/upgrade.md +363 -0
- package/templates/codex/skills/references/functions/validate.md +135 -0
- package/templates/codex/skills/references/rules/cache.md +136 -0
- package/templates/codex/skills/references/rules/scaling.md +124 -0
- package/templates/codex/skills/references/rules/state.md +201 -0
- package/templates/codex/skills/references/rules/tools.md +301 -0
- package/templates/codex/skills/references/services/attention.md +53 -0
- package/templates/codex/skills/references/services/knowledge.md +559 -0
- package/templates/codex/skills/references/services/package.md +383 -0
- package/templates/codex/skills/references/services/templates.md +390 -0
- package/templates/codex/skills/references/stages/analyze.md +191 -0
- package/templates/codex/skills/references/stages/design.md +355 -0
- package/templates/codex/skills/references/stages/develop.md +520 -0
- package/templates/codex/skills/references/stages/tweak.md +239 -0
- package/templates/codex/skills/rlm/__init__.py +39 -0
- package/templates/codex/skills/rlm/agent_orchestrator.py +422 -0
- package/templates/codex/skills/rlm/context_manager.py +366 -0
- package/templates/codex/skills/rlm/engine.py +915 -0
- package/templates/codex/skills/rlm/folding.py +391 -0
- package/templates/codex/skills/rlm/repl.py +452 -0
- package/templates/codex/skills/rlm/roles/analyzer.md +66 -0
- package/templates/codex/skills/rlm/roles/designer.md +94 -0
- package/templates/codex/skills/rlm/roles/explorer.md +43 -0
- package/templates/codex/skills/rlm/roles/implementer.md +62 -0
- package/templates/codex/skills/rlm/roles/kb_keeper.md +138 -0
- package/templates/codex/skills/rlm/roles/pkg_keeper.md +163 -0
- package/templates/codex/skills/rlm/roles/reviewer.md +74 -0
- package/templates/codex/skills/rlm/roles/synthesizer.md +90 -0
- package/templates/codex/skills/rlm/roles/tester.md +83 -0
- package/templates/codex/skills/rlm/schemas/agent_result.json +174 -0
- package/templates/codex/skills/rlm/session.py +376 -0
- package/templates/codex/skills/rlm/shared_tasks.py +370 -0
- package/templates/codex/skills/scripts/create_package.py +260 -0
- package/templates/codex/skills/scripts/list_packages.py +145 -0
- package/templates/codex/skills/scripts/migrate_package.py +399 -0
- package/templates/codex/skills/scripts/project_stats.py +438 -0
- package/templates/codex/skills/scripts/upgradewiki.py +321 -0
- package/templates/codex/skills/scripts/utils.py +596 -0
- package/templates/codex/skills/scripts/validate_package.py +309 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
HelloAGENTS 脚本工具函数
|
|
5
|
+
提供路径解析、方案包操作等通用功能
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import io
|
|
12
|
+
import functools
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_encoding():
|
|
16
|
+
"""
|
|
17
|
+
设置 stdout/stderr 编码为 UTF-8
|
|
18
|
+
解决 Windows 命令行中文输出乱码问题
|
|
19
|
+
"""
|
|
20
|
+
if sys.platform == 'win32':
|
|
21
|
+
# Windows 环境下强制使用 UTF-8
|
|
22
|
+
if hasattr(sys.stdout, 'buffer'):
|
|
23
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
24
|
+
if hasattr(sys.stderr, 'buffer'):
|
|
25
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import Optional, Tuple, List, Dict, Callable, Any
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# === 执行报告机制 ===
|
|
35
|
+
|
|
36
|
+
class ExecutionReport:
|
|
37
|
+
"""
|
|
38
|
+
脚本执行报告 - 用于 AI 降级接手
|
|
39
|
+
|
|
40
|
+
当脚本无法完成全部任务时,通过此报告告知 AI:
|
|
41
|
+
- 已完成的步骤(需质量检查)
|
|
42
|
+
- 失败点
|
|
43
|
+
- 待完成的任务
|
|
44
|
+
- 执行上下文
|
|
45
|
+
|
|
46
|
+
用法:
|
|
47
|
+
report = ExecutionReport("create_package")
|
|
48
|
+
report.set_context(feature="login", pkg_type="implementation")
|
|
49
|
+
|
|
50
|
+
# 完成一个步骤
|
|
51
|
+
report.mark_completed("创建目录", "helloagents/plan/202501_login", "检查目录是否存在")
|
|
52
|
+
|
|
53
|
+
# 遇到错误
|
|
54
|
+
report.mark_failed("加载模板 proposal.md", ["创建 proposal.md", "创建 tasks.md"])
|
|
55
|
+
|
|
56
|
+
# 输出报告
|
|
57
|
+
print(report.to_json())
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, script_name: str):
|
|
61
|
+
self.script_name = script_name
|
|
62
|
+
self.success = True
|
|
63
|
+
self.completed: List[Dict[str, str]] = [] # [{"step": "", "result": "", "verify": ""}]
|
|
64
|
+
self.failed_at: Optional[str] = None
|
|
65
|
+
self.error_message: Optional[str] = None
|
|
66
|
+
self.pending: List[str] = []
|
|
67
|
+
self.context: Dict[str, Any] = {}
|
|
68
|
+
|
|
69
|
+
def set_context(self, **kwargs):
|
|
70
|
+
"""设置执行上下文"""
|
|
71
|
+
self.context.update(kwargs)
|
|
72
|
+
|
|
73
|
+
def mark_completed(self, step: str, result: str, verify: str):
|
|
74
|
+
"""
|
|
75
|
+
标记步骤完成
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
step: 步骤描述
|
|
79
|
+
result: 执行结果(如文件路径)
|
|
80
|
+
verify: AI 质量检查方法
|
|
81
|
+
"""
|
|
82
|
+
self.completed.append({
|
|
83
|
+
"step": step,
|
|
84
|
+
"result": result,
|
|
85
|
+
"verify": verify
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
def mark_failed(self, step: str, pending: List[str], error_message: str = None):
|
|
89
|
+
"""
|
|
90
|
+
标记失败并设置待完成任务
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
step: 失败的步骤
|
|
94
|
+
pending: 待完成的任务列表
|
|
95
|
+
error_message: 错误信息
|
|
96
|
+
"""
|
|
97
|
+
self.success = False
|
|
98
|
+
self.failed_at = step
|
|
99
|
+
self.pending = pending
|
|
100
|
+
self.error_message = error_message
|
|
101
|
+
|
|
102
|
+
def mark_success(self, final_result: str = None):
|
|
103
|
+
"""标记全部完成"""
|
|
104
|
+
self.success = True
|
|
105
|
+
if final_result:
|
|
106
|
+
self.context["final_result"] = final_result
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> Dict:
|
|
109
|
+
"""转换为字典"""
|
|
110
|
+
result = {
|
|
111
|
+
"script": self.script_name,
|
|
112
|
+
"success": self.success,
|
|
113
|
+
"completed": self.completed,
|
|
114
|
+
"context": self.context
|
|
115
|
+
}
|
|
116
|
+
if not self.success:
|
|
117
|
+
result["failed_at"] = self.failed_at
|
|
118
|
+
result["error_message"] = self.error_message
|
|
119
|
+
result["pending"] = self.pending
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def to_json(self) -> str:
|
|
123
|
+
"""转换为 JSON 字符串"""
|
|
124
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
125
|
+
|
|
126
|
+
def print_report(self):
|
|
127
|
+
"""输出执行报告到 stdout"""
|
|
128
|
+
print(self.to_json())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_execution_report(script_name: str) -> ExecutionReport:
|
|
132
|
+
"""创建执行报告的工厂函数"""
|
|
133
|
+
return ExecutionReport(script_name)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# === 错误处理模板 ===
|
|
137
|
+
|
|
138
|
+
def script_error_handler(func: Callable) -> Callable:
|
|
139
|
+
"""
|
|
140
|
+
统一脚本错误处理装饰器
|
|
141
|
+
|
|
142
|
+
用法:
|
|
143
|
+
@script_error_handler
|
|
144
|
+
def main():
|
|
145
|
+
...
|
|
146
|
+
"""
|
|
147
|
+
@functools.wraps(func)
|
|
148
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
149
|
+
try:
|
|
150
|
+
return func(*args, **kwargs)
|
|
151
|
+
except KeyboardInterrupt:
|
|
152
|
+
print("\n操作已取消", file=sys.stderr)
|
|
153
|
+
sys.exit(130)
|
|
154
|
+
except FileNotFoundError as e:
|
|
155
|
+
print(f"错误: 文件未找到 - {e.filename}", file=sys.stderr)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
except PermissionError as e:
|
|
158
|
+
print(f"错误: 权限不足 - {e.filename}", file=sys.stderr)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
return wrapper
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def print_error(message: str) -> None:
|
|
167
|
+
"""输出错误信息到 stderr"""
|
|
168
|
+
print(f"❌ {message}", file=sys.stderr)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def print_success(message: str) -> None:
|
|
172
|
+
"""输出成功信息"""
|
|
173
|
+
print(f"✅ {message}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# === 路径工具 ===
|
|
177
|
+
|
|
178
|
+
# 方案包目录名称正则模式
|
|
179
|
+
PACKAGE_NAME_PATTERN = re.compile(r'^(\d{12})_(.+)$')
|
|
180
|
+
|
|
181
|
+
# HelloAGENTS 工作空间默认路径
|
|
182
|
+
DEFAULT_WORKSPACE = "helloagents"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def validate_base_path(base_path: Optional[str]) -> Path:
|
|
186
|
+
"""
|
|
187
|
+
验证并返回基础路径
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
base_path: 用户指定的路径,None 表示使用当前目录
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
验证后的 Path 对象
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: 路径不存在或不是目录
|
|
197
|
+
"""
|
|
198
|
+
if base_path is None:
|
|
199
|
+
return Path.cwd()
|
|
200
|
+
|
|
201
|
+
path = Path(base_path)
|
|
202
|
+
if not path.exists():
|
|
203
|
+
raise ValueError(f"指定的路径不存在: {base_path}")
|
|
204
|
+
if not path.is_dir():
|
|
205
|
+
raise ValueError(f"指定的路径不是目录: {base_path}")
|
|
206
|
+
return path
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_workspace_path(base_path: Optional[str] = None) -> Path:
|
|
210
|
+
"""
|
|
211
|
+
获取 HelloAGENTS 工作空间路径
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
base_path: 项目根目录,默认当前目录
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
工作空间路径 (helloagents/)
|
|
218
|
+
"""
|
|
219
|
+
base = Path(base_path) if base_path else Path.cwd()
|
|
220
|
+
return base / DEFAULT_WORKSPACE
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_plan_path(base_path: Optional[str] = None) -> Path:
|
|
224
|
+
"""获取 plan/ 目录路径"""
|
|
225
|
+
return get_workspace_path(base_path) / "plan"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_archive_path(base_path: Optional[str] = None) -> Path:
|
|
229
|
+
"""获取 archive/ 目录路径"""
|
|
230
|
+
return get_workspace_path(base_path) / "archive"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_package_name(name: str) -> Optional[Tuple[str, str]]:
|
|
234
|
+
"""
|
|
235
|
+
解析方案包目录名称
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
name: 目录名称,如 "202512191430_login"
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
(timestamp, feature) 元组,解析失败返回 None
|
|
242
|
+
"""
|
|
243
|
+
match = PACKAGE_NAME_PATTERN.match(name)
|
|
244
|
+
if match:
|
|
245
|
+
return match.group(1), match.group(2)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def generate_package_name(feature: str) -> str:
|
|
250
|
+
"""
|
|
251
|
+
生成方案包目录名称
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
feature: 功能名称
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
格式化的目录名称,如 "202512191430_login"
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: 功能名称无效(规范化后为空)
|
|
261
|
+
"""
|
|
262
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M")
|
|
263
|
+
# 规范化 feature 名称:小写、连字符替换空格
|
|
264
|
+
normalized = re.sub(r'[^a-zA-Z0-9\u4e00-\u9fff]+', '-', feature.strip().lower())
|
|
265
|
+
normalized = normalized.strip('-')
|
|
266
|
+
if not normalized:
|
|
267
|
+
raise ValueError("功能名称无效:必须包含字母、数字或中文字符")
|
|
268
|
+
return f"{timestamp}_{normalized}"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_year_month(timestamp: str) -> str:
|
|
272
|
+
"""
|
|
273
|
+
从时间戳提取年月
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
timestamp: 12位时间戳,如 "202512191430"
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
年月格式,如 "2025-12"
|
|
280
|
+
"""
|
|
281
|
+
return f"{timestamp[:4]}-{timestamp[4:6]}"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def list_packages(plan_path: Path) -> List[Dict]:
|
|
285
|
+
"""
|
|
286
|
+
列出所有方案包
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
plan_path: plan/ 目录路径
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
方案包信息列表
|
|
293
|
+
"""
|
|
294
|
+
packages = []
|
|
295
|
+
if not plan_path.exists():
|
|
296
|
+
return packages
|
|
297
|
+
|
|
298
|
+
for item in plan_path.iterdir():
|
|
299
|
+
if item.is_dir():
|
|
300
|
+
parsed = parse_package_name(item.name)
|
|
301
|
+
if parsed:
|
|
302
|
+
timestamp, feature = parsed
|
|
303
|
+
pkg_info = {
|
|
304
|
+
'name': item.name,
|
|
305
|
+
'path': item,
|
|
306
|
+
'timestamp': timestamp,
|
|
307
|
+
'feature': feature,
|
|
308
|
+
'complete': is_package_complete(item),
|
|
309
|
+
'task_count': count_tasks(item / "tasks.md")
|
|
310
|
+
}
|
|
311
|
+
packages.append(pkg_info)
|
|
312
|
+
|
|
313
|
+
# 按时间戳排序(最新在前)
|
|
314
|
+
packages.sort(key=lambda x: x['timestamp'], reverse=True)
|
|
315
|
+
return packages
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def is_package_complete(package_path: Path) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
检查方案包是否完整
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
package_path: 方案包目录路径
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
是否包含所有必需文件
|
|
327
|
+
"""
|
|
328
|
+
required_files = ['proposal.md', 'tasks.md']
|
|
329
|
+
return all((package_path / f).exists() for f in required_files)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def count_tasks(task_file: Path) -> int:
|
|
333
|
+
"""
|
|
334
|
+
统计任务数量
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
task_file: tasks.md 文件路径
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
任务数量
|
|
341
|
+
"""
|
|
342
|
+
if not task_file.exists():
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
content = task_file.read_text(encoding='utf-8')
|
|
346
|
+
# 匹配任务行: - [ ] 或 * [ ] 或 - [x] 或 - [√] 等
|
|
347
|
+
tasks = re.findall(r'^[-*]\s*\[.\]', content, re.MULTILINE)
|
|
348
|
+
return len(tasks)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def get_package_summary(package_path: Path) -> str:
|
|
352
|
+
"""
|
|
353
|
+
获取方案包摘要(从 proposal.md 提取)
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
package_path: 方案包目录路径
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
功能摘要
|
|
360
|
+
"""
|
|
361
|
+
proposal_file = package_path / "proposal.md"
|
|
362
|
+
if not proposal_file.exists():
|
|
363
|
+
return "(无描述)"
|
|
364
|
+
|
|
365
|
+
content = proposal_file.read_text(encoding='utf-8')
|
|
366
|
+
# 尝试提取第一个非标题非空行
|
|
367
|
+
lines = content.split('\n')
|
|
368
|
+
for line in lines:
|
|
369
|
+
line = line.strip()
|
|
370
|
+
if line and not line.startswith('#') and not line.startswith('---'):
|
|
371
|
+
# 截断过长的描述
|
|
372
|
+
return line[:50] + "..." if len(line) > 50 else line
|
|
373
|
+
|
|
374
|
+
return "(无描述)"
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# === 模板加载机制 ===
|
|
378
|
+
|
|
379
|
+
def get_templates_dir() -> Path:
|
|
380
|
+
"""
|
|
381
|
+
获取模板目录路径
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
模板目录的绝对路径 (assets/templates/)
|
|
385
|
+
"""
|
|
386
|
+
return Path(__file__).parent.parent / "assets" / "templates"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def load_template(template_path: str, required: bool = True) -> Optional[str]:
|
|
390
|
+
"""
|
|
391
|
+
加载模板文件内容
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
template_path: 相对于模板目录的路径,如 "plan/proposal.md"
|
|
395
|
+
required: 是否必须存在,False 时不存在返回 None
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
模板内容,或 None(当 required=False 且文件不存在时)
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
FileNotFoundError: 当 required=True 且模板不存在时
|
|
402
|
+
"""
|
|
403
|
+
full_path = get_templates_dir() / template_path
|
|
404
|
+
|
|
405
|
+
if full_path.exists():
|
|
406
|
+
return full_path.read_text(encoding='utf-8')
|
|
407
|
+
|
|
408
|
+
if required:
|
|
409
|
+
raise FileNotFoundError(f"模板文件不存在: {template_path}")
|
|
410
|
+
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def fill_template(template: str, replacements: Dict[str, str]) -> str:
|
|
415
|
+
"""
|
|
416
|
+
填充模板占位符
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
template: 模板内容
|
|
420
|
+
replacements: 占位符替换映射,如 {"{feature}": "login", "{YYYY-MM-DD}": "2025-01-19"}
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
填充后的内容
|
|
424
|
+
"""
|
|
425
|
+
content = template
|
|
426
|
+
for placeholder, value in replacements.items():
|
|
427
|
+
content = content.replace(placeholder, value)
|
|
428
|
+
return content
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def extract_template_sections(template: str, level: int = 2) -> List[str]:
|
|
432
|
+
"""
|
|
433
|
+
从模板中提取章节标题
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
template: 模板内容
|
|
437
|
+
level: 标题级别(2 表示 ##,3 表示 ###)
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
章节标题列表
|
|
441
|
+
"""
|
|
442
|
+
pattern = rf'^{"#" * level}\s+(.+)$'
|
|
443
|
+
return re.findall(pattern, template, re.MULTILINE)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def extract_required_sections(template: str) -> List[str]:
|
|
447
|
+
"""
|
|
448
|
+
从模板中提取必需章节(不含"可选"标记的章节)
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
template: 模板内容
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
必需章节的核心名称列表(去除编号和可选标记)
|
|
455
|
+
"""
|
|
456
|
+
sections = extract_template_sections(template, level=2)
|
|
457
|
+
required = []
|
|
458
|
+
|
|
459
|
+
for section in sections:
|
|
460
|
+
# 跳过包含"可选"标记的章节
|
|
461
|
+
if '可选' in section or 'optional' in section.lower():
|
|
462
|
+
continue
|
|
463
|
+
# 提取核心名称:移除编号前缀(如 "1. ")
|
|
464
|
+
core = re.sub(r'^\d+\.\s*', '', section).strip()
|
|
465
|
+
if core:
|
|
466
|
+
required.append(core)
|
|
467
|
+
|
|
468
|
+
return required
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def get_template_table_headers(template: str) -> List[List[str]]:
|
|
472
|
+
"""
|
|
473
|
+
从模板中提取表格表头
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
template: 模板内容
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
表格表头列表,每个表格是一个列名列表
|
|
480
|
+
"""
|
|
481
|
+
tables = []
|
|
482
|
+
lines = template.split('\n')
|
|
483
|
+
|
|
484
|
+
for i, line in enumerate(lines):
|
|
485
|
+
# 检测表格表头行(下一行是分隔行)
|
|
486
|
+
if line.startswith('|') and i + 1 < len(lines):
|
|
487
|
+
next_line = lines[i + 1]
|
|
488
|
+
if next_line.startswith('|') and '---' in next_line:
|
|
489
|
+
# 提取列名
|
|
490
|
+
headers = [h.strip() for h in line.split('|')[1:-1]]
|
|
491
|
+
tables.append(headers)
|
|
492
|
+
|
|
493
|
+
return tables
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TemplateLoader:
|
|
497
|
+
"""
|
|
498
|
+
模板加载器 - 提供统一的模板访问接口
|
|
499
|
+
|
|
500
|
+
用法:
|
|
501
|
+
loader = TemplateLoader()
|
|
502
|
+
|
|
503
|
+
# 加载模板
|
|
504
|
+
proposal = loader.load("plan/proposal.md")
|
|
505
|
+
|
|
506
|
+
# 填充占位符
|
|
507
|
+
content = loader.fill("plan/proposal.md", {
|
|
508
|
+
"{feature}": "login",
|
|
509
|
+
"{YYYY-MM-DD}": "2025-01-19"
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
# 获取必需章节
|
|
513
|
+
sections = loader.get_required_sections("plan/proposal.md")
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
def __init__(self):
|
|
517
|
+
self._cache: Dict[str, str] = {}
|
|
518
|
+
self._templates_dir = get_templates_dir()
|
|
519
|
+
|
|
520
|
+
def load(self, template_path: str, use_cache: bool = True) -> Optional[str]:
|
|
521
|
+
"""
|
|
522
|
+
加载模板(带缓存)
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
template_path: 相对路径
|
|
526
|
+
use_cache: 是否使用缓存
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
模板内容,不存在时返回 None
|
|
530
|
+
"""
|
|
531
|
+
if use_cache and template_path in self._cache:
|
|
532
|
+
return self._cache[template_path]
|
|
533
|
+
|
|
534
|
+
content = load_template(template_path, required=False)
|
|
535
|
+
|
|
536
|
+
if content is not None and use_cache:
|
|
537
|
+
self._cache[template_path] = content
|
|
538
|
+
|
|
539
|
+
return content
|
|
540
|
+
|
|
541
|
+
def fill(self, template_path: str, replacements: Dict[str, str]) -> Optional[str]:
|
|
542
|
+
"""
|
|
543
|
+
加载并填充模板
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
template_path: 相对路径
|
|
547
|
+
replacements: 占位符映射
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
填充后的内容,模板不存在时返回 None
|
|
551
|
+
"""
|
|
552
|
+
template = self.load(template_path)
|
|
553
|
+
if template is None:
|
|
554
|
+
return None
|
|
555
|
+
return fill_template(template, replacements)
|
|
556
|
+
|
|
557
|
+
def get_sections(self, template_path: str, level: int = 2) -> List[str]:
|
|
558
|
+
"""获取模板章节标题"""
|
|
559
|
+
template = self.load(template_path)
|
|
560
|
+
if template is None:
|
|
561
|
+
return []
|
|
562
|
+
return extract_template_sections(template, level)
|
|
563
|
+
|
|
564
|
+
def get_required_sections(self, template_path: str) -> List[str]:
|
|
565
|
+
"""获取必需章节"""
|
|
566
|
+
template = self.load(template_path)
|
|
567
|
+
if template is None:
|
|
568
|
+
return []
|
|
569
|
+
return extract_required_sections(template)
|
|
570
|
+
|
|
571
|
+
def get_table_headers(self, template_path: str) -> List[List[str]]:
|
|
572
|
+
"""获取表格表头"""
|
|
573
|
+
template = self.load(template_path)
|
|
574
|
+
if template is None:
|
|
575
|
+
return []
|
|
576
|
+
return get_template_table_headers(template)
|
|
577
|
+
|
|
578
|
+
def exists(self, template_path: str) -> bool:
|
|
579
|
+
"""检查模板是否存在"""
|
|
580
|
+
return (self._templates_dir / template_path).exists()
|
|
581
|
+
|
|
582
|
+
def clear_cache(self):
|
|
583
|
+
"""清除缓存"""
|
|
584
|
+
self._cache.clear()
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# 全局模板加载器实例
|
|
588
|
+
_template_loader: Optional[TemplateLoader] = None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def get_template_loader() -> TemplateLoader:
|
|
592
|
+
"""获取全局模板加载器实例"""
|
|
593
|
+
global _template_loader
|
|
594
|
+
if _template_loader is None:
|
|
595
|
+
_template_loader = TemplateLoader()
|
|
596
|
+
return _template_loader
|