Files
pinesound-skills-installer/SKILL.md
T
2026-06-08 13:18:18 +08:00

630 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: pinesound-skills-installer
description: PineSound 内部 Skills 安装器。通过 Gitea API 获取技能列表、安装/卸载/查询已安装的内部技能。当用户需要管理 PineSound 内部技能时使用。
allowed-tools: 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` → 验证。
```bash
# 1. 检查 .env 是否存在且非空
TOKEN_FILE=~/.codex/skills/PineSoundSkills/.env
if [ ! -f "$TOKEN_FILE" ] || [ ! -s "$TOKEN_FILE" ]; then
echo "TOKEN_MISSING"
fi
# 如果输出 TOKEN_MISSING,则需要向用户索要令牌
```
索要到令牌后,执行存储:
```bash
# 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
```
```powershell
# 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 读取令牌(后续使用)
```bash
# 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
```
```powershell
# 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 流程 |
---
## 二、平台检测
```bash
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 列表
```bash
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 确保目录存在
```bash
mkdir -p ~/.codex/skills/PineSoundSkills
```
```powershell
New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex\skills\PineSoundSkills"
```
### 4.2 克隆仓库
```bash
cd ~/.codex/skills/PineSoundSkills && git clone <仓库地址>
```
```powershell
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)
```bash
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)
```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
```bash
# Unix
SKILL_DIR=~/.codex/skills/PineSoundSkills
if [ ! -d "$SKILL_DIR/<文件夹名称>" ]; then
echo "错误: 技能目录不存在: $SKILL_DIR/<文件夹名称>"
exit 1
fi
rm -rf "$SKILL_DIR/<文件夹名称>"
echo "已卸载: <文件夹名称>"
```
```powershell
# 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 更新指定技能
```bash
# 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
```
```powershell
# 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 更新所有已安装的技能
```bash
# 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
```
```powershell
# 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)涵盖令牌获取 → 验证 → 列出 → 安装 → 更新 → 查看全过程:
```bash
#!/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
```