17 KiB
17 KiB
name, description, allowed-tools
| name | description | allowed-tools |
|---|---|---|
| pinesound-skills-installer | PineSound 内部 Skills 安装器。通过 Gitea API 获取技能列表、安装/卸载/查询已安装的内部技能。当用户需要管理 PineSound 内部技能时使用。 | Read, Grep, Edit, Write, Bash |
PineSound 内部 Skills 安装器
零、路径定义
整个 skill 统一使用以下路径:
| 含义 | Unix | Windows |
|---|---|---|
| Skills 根目录 | ~/.codex/skills/PineSoundSkills |
$env:USERPROFILE\.codex\skills\PineSoundSkills |
| 令牌文件 | ~/.codex/skills/PineSoundSkills/.env |
$env:USERPROFILE\.codex\skills\PineSoundSkills\.env |
一、令牌管理
1.1 获取令牌(首次/令牌无效时)
流程: agent 通过 AskUserQuestion 工具向用户索要令牌 → 用户输入 → 写入 .env → 验证。
# 1. 检查 .env 是否存在且非空
TOKEN_FILE=~/.codex/skills/PineSoundSkills/.env
if [ ! -f "$TOKEN_FILE" ] || [ ! -s "$TOKEN_FILE" ]; then
echo "TOKEN_MISSING"
fi
# 如果输出 TOKEN_MISSING,则需要向用户索要令牌
索要到令牌后,执行存储:
# 2. 创建目录并写入令牌
mkdir -p ~/.codex/skills/PineSoundSkills
# 用 printf 写入,避免 echo 对特殊字符的转义问题
printf '%s' "$GITEA_TOKEN" > ~/.codex/skills/PineSoundSkills/.env
# 3. Unix: 设置权限仅 owner 可读写
chmod 600 ~/.codex/skills/PineSoundSkills/.env
# 4. 验证写入成功
if [ -s ~/.codex/skills/PineSoundSkills/.env ]; then
echo "令牌已持久化存储"
else
echo "错误: 令牌写入失败"
exit 1
fi
# Windows 版
New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex\skills\PineSoundSkills"
$env:GITEA_TOKEN | Out-File -FilePath "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env" -NoNewline -Encoding ASCII
# 验证
if ((Get-Item "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env").Length -gt 0) {
Write-Host "令牌已持久化存储"
} else {
Write-Host "错误: 令牌写入失败"
exit 1
}
1.2 读取令牌(后续使用)
# Unix — 去除首尾空白,避免换行符污染 API 请求
GITEA_TOKEN=$(cat ~/.codex/skills/PineSoundSkills/.env 2>/dev/null | tr -d '\n\r' | xargs)
if [ -z "$GITEA_TOKEN" ]; then
echo "错误: 令牌文件缺失或为空,请先设置令牌"
exit 1
fi
# Windows
$GITEA_TOKEN = (Get-Content "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env" -Raw).Trim()
if ([string]::IsNullOrEmpty($GITEA_TOKEN)) {
Write-Host "错误: 令牌文件缺失或为空,请先设置令牌"
exit 1
}
1.3 令牌生命周期
| 场景 | 行为 |
|---|---|
| 首次运行 | agent 用 AskUserQuestion 索要 → 写入 .env |
| 再次运行 | 直接从 .env 读取,跳过询问 |
| 令牌失效(API 返回 401) | 提示用户令牌可能已过期,重新执行 1.1 流程 |
| 用户明确要求重新设置 | 覆盖 .env,执行 1.1 流程 |
二、平台检测
OS=$(uname -s)
case "$OS" in
Darwin) PLATFORM="macOS" ;;
Linux) PLATFORM="Linux" ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM="Windows" ;;
*) PLATFORM="Unknown" ;;
esac
echo "$PLATFORM"
根据 PLATFORM 选择对应平台的命令执行。
三、查看 Gitea 仓库中的 Skills 列表
TOKEN_FILE=~/.codex/skills/PineSoundSkills/.env
# 读取并清理令牌
if [ ! -f "$TOKEN_FILE" ] || [ ! -s "$TOKEN_FILE" ]; then
echo "错误: 令牌文件缺失或为空。请先执行令牌获取流程(见第一章)"
exit 1
fi
GITEA_TOKEN=$(cat "$TOKEN_FILE" 2>/dev/null | tr -d '\n\r' | xargs)
if [ -z "$GITEA_TOKEN" ]; then
echo "错误: 令牌为空"
exit 1
fi
# 请求 API
curl -s -H "Authorization: token $GITEA_TOKEN" \
"http://192.168.1.3:10082/api/v1/orgs/PineSoundSkills/repos" \
| python3 -c "
import json, sys
repos = json.load(sys.stdin)
if not repos:
print('(空列表 — 检查令牌或组织名称是否正确)')
for r in repos:
print(f\" {r['name']:30s} {r.get('description', '')}\")
" 2>/dev/null \
|| curl -s -H "Authorization: token $GITEA_TOKEN" \
"http://192.168.1.3:10082/api/v1/orgs/PineSoundSkills/repos"
注意:
cat读取的令牌可能包含尾部换行符,通过tr -d '\n\r'和xargs做双重清理。如果 API 返回 401,说明令牌无效或已过期。
四、安装 Skills
4.1 确保目录存在
mkdir -p ~/.codex/skills/PineSoundSkills
New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex\skills\PineSoundSkills"
4.2 克隆仓库
cd ~/.codex/skills/PineSoundSkills && git clone <仓库地址>
cd "$env:USERPROFILE\.codex\skills\PineSoundSkills"; git clone <仓库地址>
<仓库地址> 格式示例: http://192.168.1.3:10082/PineSoundSkills/<技能仓库名称>.git
4.3 完成后提示用户
技能已安装。请执行刷新技能列表操作以使新技能生效(如
/reload或重新加载会话)。
五、查看已安装的 Skills
核心要求: 只读取每个 SKILL.md 的 frontmatter(--- 到 --- 之间的元数据),不加载整个文件内容。
Unix (macOS / Linux)
SKILL_DIR=~/.codex/skills/PineSoundSkills
if [ ! -d "$SKILL_DIR" ]; then
echo "技能目录不存在: $SKILL_DIR"
echo "请先安装技能(见第四章)"
exit 0
fi
echo "=== 已安装的 PineSound Skills ==="
for dir in "$SKILL_DIR"/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
skill_file=""
[ -f "${dir}SKILL.md" ] && skill_file="${dir}SKILL.md"
[ -z "$skill_file" ] && [ -f "${dir}skill.md" ] && skill_file="${dir}skill.md"
if [ -n "$skill_file" ]; then
# 仅提取 frontmatter(第一个 --- 到第二个 --- 之间的内容)
meta=$(awk 'BEGIN{c=0} /^---/{c++; next} c==1{print} c==2{exit}' "$skill_file")
skill_name=$(echo "$meta" | grep -i '^name:' | head -1 | sed 's/^name:\s*//i' | sed 's/"//g')
skill_desc=$(echo "$meta" | grep -i '^description:' | head -1 | sed 's/^description:\s*//i' | sed 's/"//g')
echo " 📦 $name"
echo " 名称: ${skill_name:-未设置}"
echo " 说明: ${skill_desc:-无描述}"
else
echo " 📁 $name (无 SKILL.md)"
fi
echo ""
done
Windows (PowerShell)
$skillDir = "$env:USERPROFILE\.codex\skills\PineSoundSkills"
if (-not (Test-Path $skillDir)) {
Write-Host "技能目录不存在: $skillDir" -ForegroundColor Yellow
Write-Host "请先安装技能(见第四章)"
exit 0
}
Write-Host "=== 已安装的 PineSound Skills ===" -ForegroundColor Cyan
Get-ChildItem -Directory $skillDir | ForEach-Object {
$folder = $_.FullName
$name = $_.Name
$skillFile = $null
if (Test-Path "$folder\SKILL.md") { $skillFile = "$folder\SKILL.md" }
elseif (Test-Path "$folder\skill.md") { $skillFile = "$folder\skill.md" }
if ($skillFile) {
# 只读取 frontmatter(第一个 --- 到第二个 --- 之间)
$content = Get-Content $skillFile -Raw
if ($content -match '(?s)^---\s*\n(.*?)\n---') {
$frontmatter = $matches[1]
$skillName = ($frontmatter | Select-String '(?im)^name:\s*(.+)').Matches.Value -replace '(?im)^name:\s*', ''
$skillDesc = ($frontmatter | Select-String '(?im)^description:\s*(.+)').Matches.Value -replace '(?im)^description:\s*', ''
}
Write-Host " 📦 $name"
Write-Host " 名称: $(if ($skillName) { $skillName.Trim('" ') } else { '未设置' })"
Write-Host " 说明: $(if ($skillDesc) { $skillDesc.Trim('" ') } else { '无描述' })"
} else {
Write-Host " 📁 $name (无 SKILL.md)" -ForegroundColor DarkYellow
}
Write-Host ""
}
六、卸载 Skills
# Unix
SKILL_DIR=~/.codex/skills/PineSoundSkills
if [ ! -d "$SKILL_DIR/<文件夹名称>" ]; then
echo "错误: 技能目录不存在: $SKILL_DIR/<文件夹名称>"
exit 1
fi
rm -rf "$SKILL_DIR/<文件夹名称>"
echo "已卸载: <文件夹名称>"
# Windows
$path = "$env:USERPROFILE\.codex\skills\PineSoundSkills\<文件夹名称>"
if (-not (Test-Path $path)) {
Write-Host "错误: 技能目录不存在: $path"
exit 1
}
Remove-Item -Recurse -Force $path
Write-Host "已卸载: <文件夹名称>"
卸载后提示用户刷新技能列表。
七、技能更新
检查本地已安装的技能仓库与远程是否一致,如有更新则拉取最新代码。
7.1 更新指定技能
# Unix
SKILL_DIR=~/.codex/skills/PineSoundSkills
SKILL_NAME="$1" # 要更新的技能文件夹名称
if [ ! -d "$SKILL_DIR/$SKILL_NAME" ]; then
echo "错误: 技能 '$SKILL_NAME' 未安装"
exit 1
fi
cd "$SKILL_DIR/$SKILL_NAME"
if [ ! -d ".git" ]; then
echo "跳过: '$SKILL_NAME' 不是 git 仓库,无法更新"
exit 0
fi
# 获取远程最新信息
git fetch origin
# 检查本地与远程的差异
BEHIND=$(git rev-list --count HEAD..origin/$(git rev-parse --abbrev-ref HEAD) 2>/dev/null)
if [ "$BEHIND" -gt 0 ]; then
echo "检测到 '$SKILL_NAME' 落后远程 $BEHIND 个提交,正在更新..."
git pull
if [ $? -eq 0 ]; then
echo "✅ '$SKILL_NAME' 已更新到最新版本"
echo "提示: 可能需要刷新技能列表使更新生效(如 /reload)"
else
echo "❌ '$SKILL_NAME' 更新失败,请检查网络或手动处理"
exit 1
fi
else
echo "'$SKILL_NAME' 已是最新"
fi
# Windows
$skillDir = "$env:USERPROFILE\.codex\skills\PineSoundSkills"
$skillName = $args[0]
if (-not (Test-Path "$skillDir\$skillName")) {
Write-Host "错误: 技能 '$skillName' 未安装"
exit 1
}
Push-Location "$skillDir\$skillName"
if (-not (Test-Path ".git")) {
Write-Host "跳过: '$skillName' 不是 git 仓库,无法更新"
Pop-Location
exit 0
}
git fetch origin
$branch = git rev-parse --abbrev-ref HEAD
$behind = git rev-list --count HEAD..origin/$branch
if ([int]$behind -gt 0) {
Write-Host "检测到 '$skillName' 落后远程 $behind 个提交,正在更新..."
git pull
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ '$skillName' 已更新到最新版本"
Write-Host "提示: 可能需要刷新技能列表使更新生效(如 /reload)"
} else {
Write-Host "❌ '$skillName' 更新失败,请检查网络或手动处理"
exit 1
}
} else {
Write-Host "'$skillName' 已是最新"
}
Pop-Location
7.2 更新所有已安装的技能
# Unix — 遍历每个技能目录执行更新
SKILL_DIR=~/.codex/skills/PineSoundSkills
if [ ! -d "$SKILL_DIR" ]; then
echo "没有已安装的技能"
exit 0
fi
echo "=== 检查所有技能更新 ==="
echo ""
UPDATED=0
CURRENT=0
FAILED=0
for dir in "$SKILL_DIR"/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
if [ ! -d "${dir}.git" ]; then
echo " ⏭️ $name (非 git 仓库,跳过)"
continue
fi
cd "$dir"
# 保存当前分支名
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -z "$BRANCH" ]; then
echo " ⏭️ $name (无法获取分支信息,跳过)"
continue
fi
# 获取远程信息
git fetch origin 2>/dev/null
BEHIND=$(git rev-list --count HEAD..origin/$BRANCH 2>/dev/null)
if [ "$BEHIND" -gt 0 ]; then
echo " 🔄 $name (落后 $BEHIND 提交,正在更新...)"
if git pull 2>/dev/null; then
echo " ✅ 更新完成"
UPDATED=$((UPDATED + 1))
else
echo " ❌ 更新失败"
FAILED=$((FAILED + 1))
fi
else
echo " ✅ $name (已是最新)"
CURRENT=$((CURRENT + 1))
fi
done
echo ""
echo "=== 更新总结: $CURRENT 已最新, $UPDATED 已更新, $FAILED 失败 ==="
if [ "$UPDATED" -gt 0 ]; then
echo "提示: 如有技能已更新,建议刷新技能列表(如 /reload)"
fi
# Windows — 遍历每个技能目录执行更新
$skillDir = "$env:USERPROFILE\.codex\skills\PineSoundSkills"
if (-not (Test-Path $skillDir)) {
Write-Host "没有已安装的技能"
exit 0
}
Write-Host "=== 检查所有技能更新 ==="
Write-Host ""
$updated = 0
$current = 0
$failed = 0
Get-ChildItem -Directory $skillDir | ForEach-Object {
$dir = $_.FullName
$name = $_.Name
if (-not (Test-Path "$dir\.git")) {
Write-Host " ⏭️ $name (非 git 仓库,跳过)"
return
}
Push-Location $dir
$branch = git rev-parse --abbrev-ref HEAD
if ([string]::IsNullOrEmpty($branch)) {
Write-Host " ⏭️ $name (无法获取分支信息,跳过)"
Pop-Location
return
}
git fetch origin 2>$null
$behind = git rev-list --count HEAD..origin/$branch
if ([int]$behind -gt 0) {
Write-Host " 🔄 $name (落后 $behind 提交,正在更新...)"
git pull 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host " ✅ 更新完成"
$updated++
} else {
Write-Host " ❌ 更新失败"
$failed++
}
} else {
Write-Host " ✅ $name (已是最新)"
$current++
}
Pop-Location
}
Write-Host ""
Write-Host "=== 更新总结: $current 已最新, $updated 已更新, $failed 失败 ==="
if ($updated -gt 0) {
Write-Host "提示: 如有技能已更新,建议刷新技能列表(如 /reload)"
}
7.3 自动跳过非 git 目录
更新时自动检测每个技能目录是否为 git 仓库(检查 .git 目录是否存在):
- 是 git 仓库 → 执行
git fetch+git rev-list --count检查差异 → 按需git pull - 不是 git 仓库 → 跳过并提示"非 git 仓库"
- git 命令失败 → 跳过并提示错误,不中断其余技能更新
注意: 如果本地有未提交的修改,
git pull可能失败或产生合并冲突。如有冲突,agent 应手动解决或提示用户处理。
八、完整工作流示例
以下端到端脚本(Unix)涵盖令牌获取 → 验证 → 列出 → 安装 → 更新 → 查看全过程:
#!/bin/bash
SKILL_DIR=~/.codex/skills/PineSoundSkills
TOKEN_FILE="$SKILL_DIR/.env"
GITEA_API="http://192.168.1.3:10082/api/v1"
# ========== 令牌管理 ==========
# 检查令牌文件是否存在且非空
if [ ! -f "$TOKEN_FILE" ] || [ ! -s "$TOKEN_FILE" ]; then
echo "令牌文件不存在或为空。"
echo "请通过 AskUserQuestion 向用户索要 Gitea 令牌。"
# [agent 操作] 用 AskUserQuestion 获取用户输入的令牌
# 假设用户提供了令牌,存入变量 USER_TOKEN
mkdir -p "$SKILL_DIR"
printf '%s' "$USER_TOKEN" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
# 写入后验证
if [ ! -s "$TOKEN_FILE" ]; then
echo "错误: 令牌写入失败!"
exit 1
fi
echo "令牌已持久化存储到 $TOKEN_FILE"
fi
# 读取令牌(清理空白字符)
GITEA_TOKEN=$(cat "$TOKEN_FILE" 2>/dev/null | tr -d '\n\r' | xargs)
if [ -z "$GITEA_TOKEN" ]; then
echo "错误: 令牌为空"
exit 1
fi
# ========== 列出可安装 Skills ==========
echo ""
echo "=== 可安装的 Skills ==="
HTTP_CODE=$(curl -s -o /tmp/pinesound_repos.json -w "%{http_code}" \
-H "Authorization: token $GITEA_TOKEN" "$GITEA_API/orgs/PineSoundSkills/repos")
if [ "$HTTP_CODE" != "200" ]; then
echo "API 请求失败 (HTTP $HTTP_CODE),令牌可能无效或已过期"
echo "请重新执行令牌获取流程"
exit 1
fi
python3 -c "
import json
with open('/tmp/pinesound_repos.json') as f:
repos = json.load(f)
if not repos:
print('(空列表)')
for r in repos:
print(f\" {r['name']:30s} {r.get('description', '')}\")
"
rm -f /tmp/pinesound_repos.json
# ========== 已安装 Skills 处理(更新 + 查看)==========
echo ""
if [ ! -d "$SKILL_DIR" ]; then
echo "(暂无已安装的技能)"
exit 0
fi
# 更新所有已安装的技能
echo "=== 更新已安装的 Skills ==="
UPDATED=0
for dir in "$SKILL_DIR"/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
if [ ! -d "${dir}.git" ]; then
echo " ⏭️ $name (非 git)" && continue
fi
cd "$dir"
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[ -z "$BRANCH" ] && continue
git fetch origin 2>/dev/null
BEHIND=$(git rev-list --count HEAD..origin/$BRANCH 2>/dev/null)
if [ "$BEHIND" -gt 0 ]; then
echo " 🔄 $name (落后 $BEHIND 提交,更新中...)"
git pull 2>/dev/null && echo " ✅ 完成" && UPDATED=$((UPDATED + 1)) || echo " ❌ 失败"
else
echo " ✅ $name (已最新)"
fi
done
[ "$UPDATED" -gt 0 ] && echo "提示: 建议刷新技能列表(如 /reload)" || true
# 查看已安装的技能
echo ""
echo "=== 已安装的 Skills ==="
for dir in "$SKILL_DIR"/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
sf="${dir}SKILL.md"; [ -f "$sf" ] || sf="${dir}skill.md"; [ -f "$sf" ] || sf=""
if [ -n "$sf" ]; then
meta=$(awk 'BEGIN{c=0} /^---/{c++; next} c==1{print} c==2{exit}' "$sf")
sname=$(echo "$meta" | grep -i '^name:' | head -1 | sed 's/^name:\s*//i' | sed 's/"//g')
sdesc=$(echo "$meta" | grep -i '^description:' | head -1 | sed 's/^description:\s*//i' | sed 's/"//g')
echo " $name: ${sname:-$name} — ${sdesc:-无描述}"
else
echo " $name (无 SKILL.md)"
fi
done