fix: harden skill template contracts and scaffolding
All checks were successful
技能自动化发布 / release (push) Successful in 4s

Align scaffold slug replacement, Action manifest strict boundaries, metadata prune API, field permission validation, and task_logs readonly semantics with documented template contracts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 11:55:46 +08:00
parent 35c20fc0f2
commit 766d162245
17 changed files with 536 additions and 72 deletions

View File

@@ -22,7 +22,10 @@ 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` 等)
- 将文本文件中的模板占位 slug 替换为新 slug
- `your-skill-slug` → 新技能 kebab-case slug`scrape-demo-records`
- `your_skill_slug` → 连字符替换为下划线(如 `scrape_demo_records`,用于 `LOG_LOGGER_NAME` 等)
- 覆盖 `assets/actions.json``SKILL.md``scripts/util/constants.py` 等生成文件;文档中的路径示例可保留占位,但生成后的技能文件不得残留占位符
- **不**自动修改 `actions.json` 中的 CLI 业务命令;技能作者自行增删 action
- **不**自动 `git init`;打印后续 Git 与测试命令

View File

@@ -71,7 +71,8 @@ if ($targetExists) {
$excludeDirs = @(".git", ".pytest_cache", "__pycache__")
$excludeFiles = @(".env", ".env.local")
$templatePlaceholderSlug = "your-skill-slug"
$templatePlaceholderSlugKebab = "your-skill-slug"
$templatePlaceholderSlugUnderscore = "your_skill_slug"
$textFileSuffixes = @(".md", ".py", ".json", ".yaml", ".yml", ".txt", ".ini", ".ps1", ".sample")
function Copy-TemplateTree {
@@ -104,6 +105,7 @@ function Replace-TemplatePlaceholders {
[string]$Root,
[string]$Slug
)
$underscoreSlug = $Slug.Replace("-", "_")
Get-ChildItem -LiteralPath $Root -Recurse -File -Force | ForEach-Object {
$name = $_.Name
if ($excludeFiles -contains $name) { return }
@@ -115,8 +117,12 @@ function Replace-TemplatePlaceholders {
catch {
return
}
if ($text -notlike "*$templatePlaceholderSlug*") { return }
$updated = $text.Replace($templatePlaceholderSlug, $Slug)
if (
$text -notlike "*$templatePlaceholderSlugKebab*" -and
$text -notlike "*$templatePlaceholderSlugUnderscore*"
) { return }
$updated = $text.Replace($templatePlaceholderSlugKebab, $Slug)
$updated = $updated.Replace($templatePlaceholderSlugUnderscore, $underscoreSlug)
[System.IO.File]::WriteAllText($_.FullName, $updated)
}
}

View File

@@ -11,7 +11,8 @@ 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"
TEMPLATE_PLACEHOLDER_SLUG_KEBAB = "your-skill-slug"
TEMPLATE_PLACEHOLDER_SLUG_UNDERSCORE = "your_skill_slug"
TEXT_FILE_SUFFIXES = {
".md",
".py",
@@ -46,8 +47,13 @@ def _should_replace_placeholders(path: Path) -> bool:
return False
def _slug_to_underscore(slug: str) -> str:
return slug.replace("-", "_")
def _replace_template_placeholders(target: Path, slug: str) -> None:
"""将模板占位 slug 替换为新技能 slug不修改 action 业务命令结构)。"""
underscore_slug = _slug_to_underscore(slug)
for path in target.rglob("*"):
if not path.is_file():
continue
@@ -59,9 +65,14 @@ def _replace_template_placeholders(target: Path, slug: str) -> None:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
if TEMPLATE_PLACEHOLDER_SLUG not in text:
if (
TEMPLATE_PLACEHOLDER_SLUG_KEBAB not in text
and TEMPLATE_PLACEHOLDER_SLUG_UNDERSCORE not in text
):
continue
path.write_text(text.replace(TEMPLATE_PLACEHOLDER_SLUG, slug), encoding="utf-8")
updated = text.replace(TEMPLATE_PLACEHOLDER_SLUG_KEBAB, slug)
updated = updated.replace(TEMPLATE_PLACEHOLDER_SLUG_UNDERSCORE, underscore_slug)
path.write_text(updated, encoding="utf-8")
def _print_next_steps(target: Path) -> None: