chore: auto release commit (2026-07-03 18:50:07)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
All checks were successful
技能自动化发布 / release (push) Successful in 6s
This commit is contained in:
47
tools/README.md
Normal file
47
tools/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# skill-template 工具
|
||||
|
||||
## 推荐:脚手架创建新技能
|
||||
|
||||
**不要**用资源管理器整文件夹复制 `skill-template`(会带走隐藏 `.git`,导致 push 串库)。
|
||||
|
||||
Windows 首选:
|
||||
|
||||
```powershell
|
||||
cd D:\OpenClaw\client-commons\skill-template
|
||||
.\tools\scaffold_skill.ps1 -Slug my-new-skill -Destination D:\OpenClaw\client-gdcm\my-new-skill
|
||||
```
|
||||
|
||||
跨平台(Python 3.10+):
|
||||
|
||||
```bash
|
||||
python tools/scaffold_skill.py --slug my-new-skill --destination /path/to/my-new-skill
|
||||
```
|
||||
|
||||
脚本行为:
|
||||
|
||||
- 复制模板到 `-Destination`(目录末段须与 `-Slug` 一致)
|
||||
- **排除**:`.git`、`.pytest_cache`、`__pycache__`、`.env`、`.env.local`、`*.pyc`
|
||||
- 删除目标中的 `.openclaw-skill-template` 标记
|
||||
- **不**自动 `git init`;打印后续 Git 与测试命令
|
||||
|
||||
目标目录若已存在且非空,需加 `-Force` / `--force`(且目标**不得**含 `.git`)。
|
||||
|
||||
## 手工复制后的补救
|
||||
|
||||
若已整目录复制,必须先清理再初始化:
|
||||
|
||||
```powershell
|
||||
cd <新技能目录>
|
||||
Remove-Item -Recurse -Force .git
|
||||
Remove-Item -Recurse -Force .pytest_cache -ErrorAction SilentlyContinue
|
||||
Remove-Item -Force .openclaw-skill-template -ErrorAction SilentlyContinue
|
||||
git init
|
||||
git remote add origin <新技能仓库 URL>
|
||||
git remote -v
|
||||
```
|
||||
|
||||
确认 `origin` **不是** skill-template 的远端 URL。
|
||||
|
||||
## 模板标记
|
||||
|
||||
源仓库根目录有 `.openclaw-skill-template`(仅 template 保留)。业务技能不得保留该文件;`tests/test_scaffold_guard.py` 会守护。
|
||||
107
tools/scaffold_skill.ps1
Normal file
107
tools/scaffold_skill.ps1
Normal file
@@ -0,0 +1,107 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Scaffold a new skill directory from skill-template (excludes .git).
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\scaffold_skill.ps1 -Slug my-new-skill -Destination D:\OpenClaw\client-gdcm\my-new-skill
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Slug,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Test-KebabSlug([string]$Value) {
|
||||
return $Value -match '^[a-z0-9]+(-[a-z0-9]+)*$'
|
||||
}
|
||||
|
||||
function Write-ScaffoldNextSteps([string]$TargetPath) {
|
||||
Write-Host ""
|
||||
Write-Host "=== 脚手架复制完成 ===" -ForegroundColor Green
|
||||
Write-Host ("目标目录: " + $TargetPath)
|
||||
Write-Host ""
|
||||
Write-Host "请在本机继续执行(未自动 git init,避免误操作):" -ForegroundColor Yellow
|
||||
Write-Host (' cd "' + $TargetPath + '"')
|
||||
Write-Host " Remove-Item -Recurse -Force .git -ErrorAction SilentlyContinue"
|
||||
Write-Host " git init"
|
||||
Write-Host " git remote add origin <你的新技能仓库 URL>"
|
||||
Write-Host " git remote -v"
|
||||
Write-Host " python tests/run_tests.py -v"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
$TemplateRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
$TargetPath = $Destination
|
||||
|
||||
if (-not (Test-KebabSlug $Slug)) {
|
||||
throw "Slug must be kebab-case. Got: $Slug"
|
||||
}
|
||||
|
||||
$leaf = Split-Path -Leaf ($TargetPath.TrimEnd('\', '/'))
|
||||
if ($leaf -ne $Slug) {
|
||||
throw "Destination leaf '$leaf' must match -Slug '$Slug'."
|
||||
}
|
||||
|
||||
$parent = Split-Path -Parent $TargetPath
|
||||
if (-not $parent -or -not (Test-Path -LiteralPath $parent)) {
|
||||
throw "Parent directory does not exist: $parent"
|
||||
}
|
||||
|
||||
$gitPath = Join-Path $TargetPath ".git"
|
||||
if (Test-Path -LiteralPath $gitPath) {
|
||||
throw "Target already has .git at $gitPath . Remove it first or use an empty destination."
|
||||
}
|
||||
|
||||
$targetExists = Test-Path -LiteralPath $TargetPath
|
||||
if ($targetExists) {
|
||||
$entries = @(Get-ChildItem -LiteralPath $TargetPath -Force -ErrorAction SilentlyContinue)
|
||||
if ($entries.Count -gt 0 -and -not $Force) {
|
||||
throw "Target exists and is not empty: $TargetPath . Use -Force or pick an empty directory."
|
||||
}
|
||||
}
|
||||
|
||||
$excludeDirs = @(".git", ".pytest_cache", "__pycache__")
|
||||
$excludeFiles = @(".env", ".env.local")
|
||||
|
||||
function Copy-TemplateTree {
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$Dest
|
||||
)
|
||||
if (-not (Test-Path -LiteralPath $Dest)) {
|
||||
New-Item -ItemType Directory -Path $Dest -Force | Out-Null
|
||||
}
|
||||
Get-ChildItem -LiteralPath $Source -Force | ForEach-Object {
|
||||
$name = $_.Name
|
||||
if ($excludeDirs -contains $name) { return }
|
||||
if ($excludeFiles -contains $name) { return }
|
||||
if ($name -like "*.pyc") { return }
|
||||
|
||||
$srcPath = $_.FullName
|
||||
$dstPath = Join-Path $Dest $name
|
||||
if ($_.PSIsContainer) {
|
||||
Copy-TemplateTree -Source $srcPath -Dest $dstPath
|
||||
}
|
||||
else {
|
||||
Copy-Item -LiteralPath $srcPath -Destination $dstPath -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Copy template: $TemplateRoot -> $TargetPath" -ForegroundColor Cyan
|
||||
Copy-TemplateTree -Source $TemplateRoot -Dest $TargetPath
|
||||
|
||||
$marker = Join-Path $TargetPath ".openclaw-skill-template"
|
||||
if (Test-Path -LiteralPath $marker) {
|
||||
Remove-Item -LiteralPath $marker -Force
|
||||
}
|
||||
|
||||
Write-ScaffoldNextSteps -TargetPath (Resolve-Path -LiteralPath $TargetPath).Path
|
||||
106
tools/scaffold_skill.py
Normal file
106
tools/scaffold_skill.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""跨平台 skill-template 脚手架(与 scaffold_skill.ps1 行为一致)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
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]+)*$")
|
||||
|
||||
|
||||
def _ignore_copy(_dir: str, names: list[str]) -> set[str]:
|
||||
ignored: set[str] = set()
|
||||
for name in names:
|
||||
if name in EXCLUDE_DIR_NAMES or name in EXCLUDE_FILE_NAMES:
|
||||
ignored.add(name)
|
||||
elif name.endswith(".pyc"):
|
||||
ignored.add(name)
|
||||
return ignored
|
||||
|
||||
|
||||
def _print_next_steps(target: Path) -> None:
|
||||
print()
|
||||
print("=== 脚手架复制完成 ===")
|
||||
print(f"目标目录: {target}")
|
||||
print()
|
||||
print("请在本机继续执行(未自动 git init,避免误操作):")
|
||||
print(f' cd "{target}"')
|
||||
print(" Remove-Item -Recurse -Force .git -ErrorAction SilentlyContinue # 若曾手工复制漏删")
|
||||
print(" git init")
|
||||
print(" git remote add origin <你的新技能仓库 URL>")
|
||||
print(" git remote -v # 确认 remote 不是 skill-template")
|
||||
print(" python tests/run_tests.py -v")
|
||||
print()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="从 skill-template 脚手架复制新技能目录")
|
||||
parser.add_argument("--slug", required=True, help="kebab-case slug,须与目标目录名一致")
|
||||
parser.add_argument("--destination", required=True, help="新技能完整目标路径")
|
||||
parser.add_argument("--force", action="store_true", help="目标非空时允许覆盖(目标不得含 .git)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
slug = args.slug.strip()
|
||||
if not KEBAB_SLUG.match(slug):
|
||||
print(f"ERROR: Slug 必须是 kebab-case,当前: {slug}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
target = Path(args.destination).resolve()
|
||||
if target.name != slug:
|
||||
print(
|
||||
f"ERROR: 目标目录末段 '{target.name}' 与 slug '{slug}' 不一致。",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
parent = target.parent
|
||||
if not parent.is_dir():
|
||||
print(f"ERROR: 父目录不存在: {parent}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if (target / ".git").exists():
|
||||
print(
|
||||
f"ERROR: 目标已存在 .git,请先删除: {target / '.git'}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
template_root = Path(__file__).resolve().parent.parent
|
||||
if target.exists():
|
||||
entries = list(target.iterdir())
|
||||
if entries and not args.force:
|
||||
print(
|
||||
f"ERROR: 目标目录已存在且非空: {target}\n请加 --force 或换空目录。",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
print(f"复制模板: {template_root} -> {target}")
|
||||
for item in template_root.iterdir():
|
||||
name = item.name
|
||||
if name in EXCLUDE_DIR_NAMES or name in EXCLUDE_FILE_NAMES:
|
||||
continue
|
||||
if name.endswith(".pyc"):
|
||||
continue
|
||||
dest = target / name
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, dest, ignore=_ignore_copy, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
|
||||
marker = target / ".openclaw-skill-template"
|
||||
if marker.is_file():
|
||||
marker.unlink()
|
||||
|
||||
_print_next_steps(target)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user