From f4404c6fb6b2a55c2a9228285dd52bec9275f099 Mon Sep 17 00:00:00 2001 From: Pine Date: Mon, 8 Jun 2026 13:18:18 +0800 Subject: [PATCH] init --- SKILL.md | 629 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 629 insertions(+) create mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..e91aa7c --- /dev/null +++ b/SKILL.md @@ -0,0 +1,629 @@ +--- +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 +```