--- name: pinesound-skills-installer description: PineSound 内部 Skills 安装器。通过 Gitea API 获取技能列表、安装/卸载/查询已安装的内部技能。当用户需要管理 PineSound 内部技能时使用。 allowed-tools: Read, Grep, Edit, Write, Bash --- # PineSound 内部 Skills 安装器 > **范围说明**:本 Skill 仅关注 PineSound 内部 Skills(`PineSoundSkills` 组织下的仓库)。当用户查询或管理技能时,**只返回 PineSound 内部 Skills**,不涉及其他来源的技能。如果 Gitea 不可达,fallback 也只列出本地已安装的 PineSound 内部技能。 --- ## 零、路径定义 > **⚠️ 需要请求 escalate 权限**:本 Skill 需要在 `~/.codex/skills/PineSoundSkills/` 下创建目录和 `.env` 令牌文件,以及通过 `curl` 访问内网 Gitea 服务。该路径不在沙箱默认 `writable_roots` 中,内网地址不再默认网络允许列表。**所有涉及 `mkdir`、`printf`(写入 .env)、`curl`(请求 Gitea API)的命令,都必须通过 `sandbox_permissions: "require_escalated"` 请求 escalate 权限后执行。** 整个 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 向用户索要令牌 → 用户输入 → 先 escalate → 写入 `.env` → 验证。 > **需 escalate**: 以下命令涉及文件系统写入。 ```bash # 1. 确保目录存在,再检查 .env 中是否包含 GITEA_TOKEN TOKEN_FILE=~/.codex/skills/PineSoundSkills/.env mkdir -p "$(dirname "$TOKEN_FILE")" # KEY=VALUE 格式:检查是否存在且包含 GITEA_TOKEN 键 if [ ! -f "$TOKEN_FILE" ] || ! grep -q "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null; then echo "TOKEN_MISSING" fi # 如果输出 TOKEN_MISSING,则需要向用户索要令牌 ``` 索要到令牌后,执行存储: ```bash # 2. 创建目录并写入令牌(KEY=VALUE 格式) mkdir -p ~/.codex/skills/PineSoundSkills # 用 KEY=VALUE 格式写入,避免 echo 对特殊字符的转义问题 printf "GITEA_TOKEN=%s\n" "$GITEA_TOKEN" > ~/.codex/skills/PineSoundSkills/.env # 3. Unix: 设置权限仅 owner 可读写 chmod 600 ~/.codex/skills/PineSoundSkills/.env # 4. 验证写入成功 if grep -q "^GITEA_TOKEN=" ~/.codex/skills/PineSoundSkills/.env 2>/dev/null; then echo "令牌已持久化存储(KEY=VALUE 格式)" else echo "错误: 令牌写入失败" exit 1 fi ``` ```powershell # Windows 版(KEY=VALUE 格式) New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex\skills\PineSoundSkills" "GITEA_TOKEN=$env:GITEA_TOKEN" | Out-File -FilePath "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env" -Encoding ASCII # 验证 if (Select-String -Path "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env" -Pattern "^GITEA_TOKEN=" -Quiet) { Write-Host "令牌已持久化存储(KEY=VALUE 格式)" } else { Write-Host "错误: 令牌写入失败" exit 1 } ``` ### 1.2 读取令牌(后续使用) ```bash # Unix — 确保目录存在后再读取令牌(按 KEY 提取) mkdir -p ~/.codex/skills/PineSoundSkills # 从 .env 中按 KEY 提取 GITEA_TOKEN(KEY=VALUE 格式) GITEA_TOKEN=$(grep "^GITEA_TOKEN=" ~/.codex/skills/PineSoundSkills/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\n\r' | xargs) if [ -z "$GITEA_TOKEN" ]; then echo "错误: .env 中缺少 GITEA_TOKEN,请先设置令牌" exit 1 fi ``` ```powershell # Windows — 确保目录存在后再读取令牌(按 KEY 提取) $null = New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex\skills\PineSoundSkills" 2>$null $GITEA_TOKEN = (Select-String -Path "$env:USERPROFILE\.codex\skills\PineSoundSkills\.env" -Pattern "^GITEA_TOKEN=" | ForEach-Object { $_.Line.Split("=", 2)[1].Trim() }) if ([string]::IsNullOrEmpty($GITEA_TOKEN)) { Write-Host "错误: .env 中缺少 GITEA_TOKEN,请先设置令牌" exit 1 } ``` ### 1.3 令牌生命周期 | 场景 | 行为 | |------|------| | 首次运行 | agent 索要 → escalate(fs 写入)→ 写入 `.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 列表 > **需 escalate**: 以下步骤需要同时 escalate 文件系统写入(mkdir/读取 .env)和网络访问(curl 请求内网 Gitea)。 ```bash TOKEN_FILE=~/.codex/skills/PineSoundSkills/.env # 确保目录存在 mkdir -p "$(dirname "$TOKEN_FILE")" # 按 KEY 读取 GITEA_TOKEN if [ ! -f "$TOKEN_FILE" ] || ! grep -q "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null; then echo "错误: .env 中缺少 GITEA_TOKEN。请先执行令牌获取流程(见第一章)" exit 1 fi GITEA_TOKEN=$(grep "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\n\r' | xargs) if [ -z "$GITEA_TOKEN" ]; then echo "错误: 令牌为空" exit 1 fi # 请求 API(内网 Gitea,需 escalate 网络权限) 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,说明令牌无效或已过期。 ### 3.1 网络不可用时的 fallback(仅列出 PineSound 内部技能) 如果 `curl` 返回 `exit code 7`(连接失败)、`exit code 6`(DNS 解析失败)或 HTTP 状态码非 200,按以下方式处理: 1. 先检查 `~/.codex/skills/PineSoundSkills/` 下是否有已安装的 PineSound 技能 2. 向用户报告 Gitea 连不通,仅列出本地已安装的 PineSound 内部技能 3. 询问用户是否要重试连接或手动操作 **注意**:不要在输出中混入其他来源的技能(如系统 skills、社区 skills 等),只显示 PineSound 内部的。 ```bash # 当 Gitea API 不可达时的 fallback:列出本地已安装的 PineSound 内部技能 echo "Gitea 服务 (192.168.1.3:10082) 当前不可达。" echo "" echo "=== 已安装的 PineSound 内部 Skills ===" SKILL_DIR=~/.codex/skills/PineSoundSkills if [ -d "$SKILL_DIR" ]; then for d in "$SKILL_DIR"/*/; do [ -d "$d" ] || continue name=$(basename "$d") [ -f "${d}SKILL.md" ] && desc=$(grep '^description:' "${d}SKILL.md" | head -1 | sed 's/^description:\s*//i' | sed 's/"//g') || desc="" echo " $name${desc:+ — $desc}" done else echo "(暂未安装任何 PineSound 内部技能)" fi ``` ```powershell # Windows fallback — 仅列出 PineSound 内部技能 Write-Host "Gitea 服务 (192.168.1.3:10082) 当前不可达。" Write-Host "" Write-Host "=== 已安装的 PineSound 内部 Skills ===" $skillDir = "$env:USERPROFILE\.codex\skills\PineSoundSkills" if (Test-Path $skillDir) { foreach ($d in Get-ChildItem -Directory $skillDir) { $name = $d.Name $sf = "$($d.FullName)\SKILL.md" $desc = "" if (Test-Path $sf) { $match = Select-String -Path $sf -Pattern '(?im)^description:\s*(.+)' if ($match) { $desc = $match.Matches.Value -replace '(?im)^description:\s*', '' } } Write-Host " $name$(if ($desc) { " -- $desc" })" } } else { Write-Host "(暂未安装任何 PineSound 内部技能)" } ``` --- ## 四、安装 Skills > **需 escalate**: 以下涉及文件系统写入和 git 克隆(网络访问)。 ### 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" # ========== 令牌管理 ========== # 确保目录存在 mkdir -p "$(dirname "$TOKEN_FILE")" # 检查 .env 是否存在且包含 GITEA_TOKEN(KEY=VALUE 格式) if [ ! -f "$TOKEN_FILE" ] || ! grep -q "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null; then echo "令牌文件不存在或未设置 GITEA_TOKEN。" echo "请通过 AskUserQuestion 向用户索要 Gitea 令牌。" # [agent 操作] 用 AskUserQuestion 获取用户输入的令牌 # 假设用户提供了令牌,存入变量 USER_TOKEN mkdir -p "$SKILL_DIR" printf "GITEA_TOKEN=%s\n" "$USER_TOKEN" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" # 写入后验证 if grep -q "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null; then echo "令牌已持久化存储到 $TOKEN_FILE(KEY=VALUE 格式)" else echo "错误: 令牌写入失败!" exit 1 fi fi # 按 KEY 读取令牌(清理空白字符) GITEA_TOKEN=$(grep "^GITEA_TOKEN=" "$TOKEN_FILE" 2>/dev/null | head -1 | cut -d= -f2- | 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 "请重新执行令牌获取流程" # fallback:列出本地已安装的 PineSound 内部技能 echo "" echo "=== 已安装的 PineSound 内部 Skills ===" SKILL_DIR=~/.codex/skills/PineSoundSkills if [ -d "$SKILL_DIR" ]; then for d in "$SKILL_DIR"/*/; do [ -d "$d" ] || continue name=$(basename "$d") [ -f "${d}SKILL.md" ] && desc=$(grep '^description:' "${d}SKILL.md" | head -1 | sed 's/^description:\s*//i' | sed 's/"//g') || desc="" echo " $name${desc:+ — $desc}" done else echo "(暂未安装任何 PineSound 内部技能)" fi 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 ```