feat: standardize skill data management and actions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,10 +22,24 @@ python tools/scaffold_skill.py --slug my-new-skill --destination /path/to/my-new
|
||||
- 复制模板到 `-Destination`(目录末段须与 `-Slug` 一致)
|
||||
- **排除**:`.git`、`.pytest_cache`、`__pycache__`、`.env`、`.env.local`、`*.pyc`
|
||||
- 删除目标中的 `.openclaw-skill-template` 标记
|
||||
- 将文本文件中的 `your-skill-slug` 替换为新 slug(含 `assets/actions.json`、`SKILL.md`、`constants.py` 等)
|
||||
- **不**自动修改 `actions.json` 中的 CLI 业务命令;技能作者自行增删 action
|
||||
- **不**自动 `git init`;打印后续 Git 与测试命令
|
||||
|
||||
目标目录若已存在且非空,需加 `-Force` / `--force`(且目标**不得**含 `.git`)。
|
||||
|
||||
## 脚手架复制后必须保留 / 修改 / 可删除的文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `assets/actions.json` | 默认复制;不需要宿主 Action 入口时可删除 |
|
||||
| `references/ACTIONS.md` | 契约文档;无 `actions.json` 时可保留作参考或按需精简 |
|
||||
| `assets/schemas/skill-actions.schema.json` | manifest 规范;无 action 时可保留 |
|
||||
| `tests/test_actions_manifest.py` | 若删除 `actions.json`,应同步删除或改写此测试 |
|
||||
| `tests/test_data_management_contract.py` | 通用契约测试;建议保留,按需扩展本技能表结构测试 |
|
||||
|
||||
`demo_items` 等示例表仅存在于模板测试中,**不会**作为 scaffold 后的默认业务表复制进新技能。
|
||||
|
||||
## 手工复制后的补救
|
||||
|
||||
若已整目录复制,必须先清理再初始化:
|
||||
|
||||
@@ -34,6 +34,7 @@ function Write-ScaffoldNextSteps([string]$TargetPath) {
|
||||
Write-Host " git init"
|
||||
Write-Host " git remote add origin <你的新技能仓库 URL>"
|
||||
Write-Host " git remote -v"
|
||||
Write-Host " # 确认 assets/actions.json 中 skill 已替换为新 slug;不需要 Action 时可删除该文件"
|
||||
Write-Host " python tests/run_tests.py -v"
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -70,6 +71,8 @@ if ($targetExists) {
|
||||
|
||||
$excludeDirs = @(".git", ".pytest_cache", "__pycache__")
|
||||
$excludeFiles = @(".env", ".env.local")
|
||||
$templatePlaceholderSlug = "your-skill-slug"
|
||||
$textFileSuffixes = @(".md", ".py", ".json", ".yaml", ".yml", ".txt", ".ini", ".ps1", ".sample")
|
||||
|
||||
function Copy-TemplateTree {
|
||||
param(
|
||||
@@ -96,6 +99,28 @@ function Copy-TemplateTree {
|
||||
}
|
||||
}
|
||||
|
||||
function Replace-TemplatePlaceholders {
|
||||
param(
|
||||
[string]$Root,
|
||||
[string]$Slug
|
||||
)
|
||||
Get-ChildItem -LiteralPath $Root -Recurse -File -Force | ForEach-Object {
|
||||
$name = $_.Name
|
||||
if ($excludeFiles -contains $name) { return }
|
||||
$ext = $_.Extension.ToLowerInvariant()
|
||||
if ($textFileSuffixes -notcontains $ext -and $name -ne ".env.example") { return }
|
||||
try {
|
||||
$text = [System.IO.File]::ReadAllText($_.FullName)
|
||||
}
|
||||
catch {
|
||||
return
|
||||
}
|
||||
if ($text -notlike "*$templatePlaceholderSlug*") { return }
|
||||
$updated = $text.Replace($templatePlaceholderSlug, $Slug)
|
||||
[System.IO.File]::WriteAllText($_.FullName, $updated)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Copy template: $TemplateRoot -> $TargetPath" -ForegroundColor Cyan
|
||||
Copy-TemplateTree -Source $TemplateRoot -Dest $TargetPath
|
||||
|
||||
@@ -104,4 +129,6 @@ if (Test-Path -LiteralPath $marker) {
|
||||
Remove-Item -LiteralPath $marker -Force
|
||||
}
|
||||
|
||||
Replace-TemplatePlaceholders -Root $TargetPath -Slug $Slug
|
||||
|
||||
Write-ScaffoldNextSteps -TargetPath (Resolve-Path -LiteralPath $TargetPath).Path
|
||||
|
||||
@@ -11,6 +11,19 @@ from pathlib import Path
|
||||
EXCLUDE_DIR_NAMES = {".git", ".pytest_cache", "__pycache__"}
|
||||
EXCLUDE_FILE_NAMES = {".env", ".env.local"}
|
||||
KEBAB_SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug"
|
||||
TEXT_FILE_SUFFIXES = {
|
||||
".md",
|
||||
".py",
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
".txt",
|
||||
".ini",
|
||||
".ps1",
|
||||
".sample",
|
||||
".env.example",
|
||||
}
|
||||
|
||||
|
||||
def _ignore_copy(_dir: str, names: list[str]) -> set[str]:
|
||||
@@ -23,6 +36,34 @@ def _ignore_copy(_dir: str, names: list[str]) -> set[str]:
|
||||
return ignored
|
||||
|
||||
|
||||
def _should_replace_placeholders(path: Path) -> bool:
|
||||
if path.name in EXCLUDE_FILE_NAMES:
|
||||
return False
|
||||
if path.suffix.lower() in TEXT_FILE_SUFFIXES:
|
||||
return True
|
||||
if path.name == ".env.example":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _replace_template_placeholders(target: Path, slug: str) -> None:
|
||||
"""将模板占位 slug 替换为新技能 slug(不修改 action 业务命令结构)。"""
|
||||
for path in target.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.name in EXCLUDE_FILE_NAMES:
|
||||
continue
|
||||
if path.suffix.lower() not in TEXT_FILE_SUFFIXES and path.name != ".env.example":
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if TEMPLATE_PLACEHOLDER_SLUG not in text:
|
||||
continue
|
||||
path.write_text(text.replace(TEMPLATE_PLACEHOLDER_SLUG, slug), encoding="utf-8")
|
||||
|
||||
|
||||
def _print_next_steps(target: Path) -> None:
|
||||
print()
|
||||
print("=== 脚手架复制完成 ===")
|
||||
@@ -34,6 +75,7 @@ def _print_next_steps(target: Path) -> None:
|
||||
print(" git init")
|
||||
print(" git remote add origin <你的新技能仓库 URL>")
|
||||
print(" git remote -v # 确认 remote 不是 skill-template")
|
||||
print(" # 确认 assets/actions.json 中 skill 已替换为新 slug;不需要 Action 时可删除该文件")
|
||||
print(" python tests/run_tests.py -v")
|
||||
print()
|
||||
|
||||
@@ -98,6 +140,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if marker.is_file():
|
||||
marker.unlink()
|
||||
|
||||
_replace_template_placeholders(target, slug)
|
||||
|
||||
_print_next_steps(target)
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user