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

17 KiB
Raw Blame History

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