--- 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 ```