feat: standardize skill data management and actions

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 11:23:14 +08:00
parent 48a86e56f1
commit 35c20fc0f2
18 changed files with 1534 additions and 68 deletions

View File

@@ -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 后的默认业务表复制进新技能。
## 手工复制后的补救
若已整目录复制,必须先清理再初始化:

View File

@@ -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

View File

@@ -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