Initial commit: add all skills

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:48:53 +08:00
commit dea5db60d9
31 changed files with 3111 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.pyo
.env
*.log

View File

@@ -0,0 +1,179 @@
---
name: cli-docs
description: |
将任意 CLI 工具的所有子命令自动整理成一份结构化中文 **Markdown 文档文件**.md
包含目录、命令总览表格、每条命令的中文详解和示例,适合存档、分享或离线阅读。
**触发条件(需同时满足)**
- 用户明确提到"markdown"、"文档"、"guide"、"指南"、"整理文档"、"生成文档"、"总结文档"等词语
- 用户的最终目的是得到一个 **文件**.md而非一个可执行脚本
典型触发示例:
- "帮我把 git CLI 整理成 markdown"
- "生成 docker 的中文命令文档"
- "把 kubectl 的命令总结成一份 guide"
- "summarize X cli to markdown"
- "把 npm 命令整理成文档文件"
**不触发本 skill 的情况**(交给 command-help-zh-skill 处理):
- 用户只想在终端里看中文帮助(无需保存文件)
- 用户想要一个可执行的 bash 脚本 / help 脚本
- 用户想翻译单条命令的 --help 输出
- 用户说"帮我写一个 X 的中文 help 脚本"
---
# CLI Docs Skill
将命令行工具的帮助信息自动整理成一份中文 Markdown 指南,包含版本号、目录、命令总数统计以及每个命令的中文说明。
---
## 第一步:确定工具名称
从用户的请求中提取目标工具名(例如 `git``docker``kubectl``npm`)。如果不确定,先询问用户。
---
## 第二步:获取版本信息
按顺序尝试以下命令,取第一个成功的结果:
```
<tool> --version
<tool> -v
<tool> version
<tool> -version
```
从输出中提取版本号和版本名称(如有,例如 `Docker version 24.0.5, build ced0996`)。如果所有命令都失败,记录"版本信息获取失败"。
---
## 第三步:获取顶级命令列表
按顺序尝试以下命令:
```
<tool> --help
<tool> -h
<tool> help
<tool> -help
```
从输出中解析出所有顶级命令subcommands。注意
- 有些工具把命令分组展示(如 `Management Commands` / `Commands`),需要合并所有组
- 有些工具的命令用缩进或列表格式呈现,注意识别命令名和其简短描述
- 全局选项flags`--verbose``-h`)不算命令,不要列入
---
## 第四步:获取每个子命令的详细帮助
对每个顶级命令,运行:
```
<tool> <subcommand> --help
```
如果失败,尝试:
```
<tool> help <subcommand>
<tool> <subcommand> -h
```
收集每个命令的:
- 功能说明(一到两句话)
- 用法usage
- 重要参数/选项(不需要列出全部,选最常用的)
- 如果该命令本身还有子命令,也递归列出(最多两层深度)
**子命令数量控制**:如果工具命令非常多(超过 30 个),先列出最核心的命令,并在文档末尾注明"仅列出核心命令,完整列表请运行 `<tool> --help`"。
---
## 第五步:生成 Markdown 文档
### 文档结构模板
```markdown
# <工具名> 命令行指南
> **版本**<版本号><版本名称,如有>
> **生成时间**<当前日期>
---
## 目录
- [简介](#简介)
- [命令总览](#命令总览)
- [命令详解](#命令详解)
- [command1](#command1)
- [command2](#command2)
...
---
## 简介
<用 2-3 句话中文介绍该工具是什么、主要用途是什么。从 --help 输出的描述中提炼,结合你对该工具的了解。>
---
## 命令总览
**N** 个顶级命令,**M** 个子命令(含二级命令)。
| 命令 | 简介 |
|------|------|
| `command1` | 一句话中文描述 |
| `command2` | 一句话中文描述 |
...
---
## 命令详解
### command1
**功能**<中文说明2-4 句,解释这个命令做什么、适合什么场景>
**用法**
```
<tool> command1 [选项] [参数]
```
**常用选项**
| 选项 | 说明 |
|------|------|
| `--flag1` | 说明 |
| `--flag2` | 说明 |
**示例**
```bash
<tool> command1 --flag example
```
---
(每个命令重复上面的格式)
```
### 写作要求
- **全部用中文**:命令说明、功能介绍、选项描述都用中文。命令名称、选项名称本身保持英文原样。
- **忠实原文**:中文描述要忠实于 `--help` 输出的含义,不要过度发挥或编造。
- **通俗易懂**:把技术文档翻译成普通用户能理解的语言,必要时加一句使用场景。
- **格式整洁**表格对齐代码块用正确语言标记shell/bash
---
## 第六步:输出文件
将生成的 Markdown 内容写入文件 `<tool>-guide.md`,保存在**用户当前工作目录**(即 Claude Code 运行的目录)。
完成后告知用户:
- 文件保存路径(完整路径)
- 共整理了多少个顶级命令、多少个子命令
- 如有获取失败的命令,列出说明

View File

@@ -0,0 +1,50 @@
{
"skill_name": "cli-docs",
"evals": [
{
"id": 1,
"prompt": "帮我把 git 命令行工具总结成 markdown 文档指南,需要有目录,显示版本号,列出所有命令和子命令,每个命令写中文介绍",
"expected_output": "生成 git-guide.md 文件包含版本信息、目录TOC、命令总数统计、每个 git 命令(如 commit/push/pull/clone 等)的中文说明和用法示例",
"files": [],
"assertions": [
{"text": "guide_file_exists: git-guide.md 文件已生成"},
{"text": "has_version_info: 文件包含 git 版本号(如 2.x.x 格式字符串)"},
{"text": "has_toc: 文件包含目录区块(## 目录 或 TOC 链接列表)"},
{"text": "has_command_count: 文件包含命令数量统计(如 '共 N 个命令'"},
{"text": "has_code_blocks: 文件包含 markdown 代码块(```"},
{"text": "has_chinese_content: 文件包含中文字符说明"},
{"text": "is_substantial: 文件内容超过 3000 字符"}
]
},
{
"id": 2,
"prompt": "帮我把 curl 命令行工具整理成 markdown 文档,要有目录和版本号,每个命令或选项配中文说明",
"expected_output": "生成 curl-guide.md 文件,包含:版本信息、目录、常用选项的中文说明(如 -X、-H、-d、-o 等),以及使用示例",
"files": [],
"assertions": [
{"text": "guide_file_exists: curl-guide.md 文件已生成"},
{"text": "has_version_info: 文件包含 curl 版本号"},
{"text": "has_toc: 文件包含目录区块"},
{"text": "has_command_count: 文件包含选项/命令数量统计"},
{"text": "has_code_blocks: 文件包含 markdown 代码块(```"},
{"text": "has_chinese_content: 文件包含中文字符说明"},
{"text": "is_substantial: 文件内容超过 3000 字符"}
]
},
{
"id": 3,
"prompt": "npm 有哪些命令?帮我整理成一个 markdown 文档,中文说明,要有版本号和目录",
"expected_output": "生成 npm-guide.md 文件包含npm 版本号、目录、npm 的顶级命令(如 install/run/publish/init 等)的中文介绍、参数说明和用法示例",
"files": [],
"assertions": [
{"text": "guide_file_exists: npm-guide.md 文件已生成"},
{"text": "has_version_info: 文件包含 npm 版本号"},
{"text": "has_toc: 文件包含目录区块"},
{"text": "has_command_count: 文件包含命令数量统计"},
{"text": "has_code_blocks: 文件包含 markdown 代码块(```"},
{"text": "has_chinese_content: 文件包含中文字符说明"},
{"text": "is_substantial: 文件内容超过 3000 字符"}
]
}
]
}

View File

@@ -0,0 +1,67 @@
---
name: command-help-zh-skill
description: |
为任意 CLI 命令生成一个独立的 **Bash 可执行脚本**`<command>-help`
运行该脚本时会调用原始命令的 --help将说明文字翻译成中文并用 ANSI 黄色高亮,
命令名/选项/参数保持原色,脚本可直接放入 PATH 供日常终端使用。
**触发条件(满足任一即可)**
- 用户明确提到"bash 脚本"、"help 脚本"、"中文 help"、"中文高亮帮助"
- 用户想要一个可放入 PATH、在终端直接运行的脚本
- 用户说"帮我翻译 X 的 --help"但没有要求保存为 .md 文档
- 用户说"帮我写一个 X 的中文 help 脚本 / 帮助脚本"
典型触发示例:
- "帮我写一个 git 的中文 help 脚本"
- "给 docker 做一个终端里用的中文帮助脚本"
- "翻译 kubectl --help 输出,高亮说明部分"
- "生成一个 tea-help bash 脚本,说明用中文显示"
**不触发本 skill 的情况**(交给 cli-docs 处理):
- 用户明确说要生成 .md 文档 / markdown 文件
- 用户想要存档或分享的完整命令参考文档
---
# 命令帮助中文高亮
## 适用场景
用户要把任意命令的 `--help` / `-h` 帮助页做成中文版本,并且要求:
- 只高亮“介绍 / 描述”文字
- 命令名、参数名、选项名保持普通颜色
- 脚本必须是单文件 `bash`
- 不能依赖复杂工具链,优先只用 `printf``case``cat`
## 处理流程
1. 先确认目标命令名,例如 `codex``git``tea``gh`,或由用户提供 `--help` 输出。
2. 如果没有原始帮助内容,就先运行 `<command> --help` 获取输出。
3. 翻译成自然、简洁的中文。
4. 保留原始结构:
- 标题
- 简介
- 用法
- 命令
- 参数
- 选项
5. 输出时只让说明文字使用 ANSI 黄色。
- 例如 `\033[1;33m`
- 结束后恢复默认颜色
6. 命令名、参数、选项、括号内容保持普通颜色,不要整行染色。
7. 生成可直接放到全局 PATH 的脚本,默认命名为 `<command>-help`;如果用户要求或命名冲突,再用 `<command>-help-zh`
## 实现要求
- 必须是独立脚本,不能依赖仓库内外的其他文件。
- 脚本开头应固定目标命令名,执行时再调用原始命令的 `--help`
- 推荐使用 `printf` 逐行输出,避免把整段文本一起染色。
- 如果需要分隔“左侧标签”和“右侧说明”,先打印标签,再单独打印黄色说明。
- 保持输出整洁,避免多余前导空格。
## 输出标准
- 中文表达自然,优先清晰准确。
- 说明部分高亮黄色。
- 可以直接复制到 `/usr/local/bin/` 之类的位置使用。
- 如果用户指定了固定命令名,按用户要求命名。

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Command Help Chinese Highlight"
short_description: "Translate any CLI help text into Chinese with yellow-highlighted descriptions only"
default_prompt: "Create a standalone no-dependency Bash help wrapper for the specified command. Translate its help text into Chinese, keep command names and arguments uncolored, and highlight only descriptions in yellow."

1
gitea-tea-skill/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

4
gitea-tea-skill/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
*.py[cod]
.venv/
venv/

232
gitea-tea-skill/SKILL.md Normal file
View File

@@ -0,0 +1,232 @@
---
name: gitea-tea-skill
description: 使用 `tea` CLI 或直接调用 Gitea API 与 https://git.shumengya.top 交互。支持仓库、Issue、PR、Release 管理。默认脚本语言为 Python辅助脚本零外部依赖。
---
# 萌芽 Gitea 操作指南
`tea` 是 Gitea 官方 CLI同时本技能提供纯 Python 辅助脚本(零依赖),两者互补使用。
---
## 萌芽 Gitea 服务器信息
| 项目 | 值 |
|------|-----|
| **地址** | https://git.shumengya.top |
| **用户名** | `shumengya` |
| **默认令牌** | `dbc5dfe55b19e9a32f685a97f126cafc2ef76449` |
| **仓库基础路径** | `https://git.shumengya.top/shumengya` |
| **SSH 地址** | `git@git.shumengya.top:shumengya/<仓库名>.git` |
| **SSH 端口** | `8022` |
| **API 基础路径** | `https://git.shumengya.top/api/v1` |
---
## 方式一tea CLI
### 前置检查
1. **检查 tea 是否已安装**`tea --version`
- 不存在则安装最新版:
- Linux`wget -qO- https://gitea.com/gitea/tea/releases/latest/download/tea-linux-amd64.tar.gz | tar -xz -C /usr/local/bin/ tea`
- macOS`brew install tea`
- 如果 tea 版本太旧(如 0.1.0-dev建议升级
```bash
# 备份旧 config
cp ~/.tea/tea.yml ~/.tea/tea.yml.bak
# 下载新版覆盖
wget -qO- https://gitea.com/gitea/tea/releases/latest/download/tea-linux-amd64.tar.gz | tar -xz -C /usr/local/bin/ tea
```
2. **登录**
```bash
tea login add --url https://git.shumengya.top --token dbc5dfe55b19e9a32f685a97f126cafc2ef76449 --name gitea-shumengya
```
配置文件路径:`~/.tea/tea.yml`Linux/macOS
3. **已知问题与解决方案**
- 旧版 tea0.1.0-dev的 `-l` flag 可能无效
- **如果 tea 命令异常直接改用方式二API或方式三Python 脚本)**
### 常用命令
```bash
# 仓库
tea repos ls # 列出仓库
tea repos create --name <仓库名> # 创建仓库
tea repos delete shumengya/<仓库名> # 删除仓库
# Issue
tea issues list --state all -o json # 列出所有 IssueJSON 格式)
tea issues create -t "标题" -d "描述"
tea issues close <编号>
tea issues reopen <编号>
# Pull Request
tea pulls list # 列出 PR
tea pulls create -t "标题" -b main -d "描述"
tea pulls approve <编号>
tea pulls merge <编号>
tea pulls close <编号>
# Release
tea releases list
tea releases create --tag v1.0.0 --title "v1.0.0" --note "更新说明"
```
---
## 方式二Gitea REST APIcurl
当 tea 不可用时,直接用 curl 操作。令牌已配置:
```bash
TOKEN="dbc5dfe55b19e9a32f685a97f126cafc2ef76449"
API="https://git.shumengya.top/api/v1"
```
### 仓库操作
```bash
# 列出所有仓库
curl -s -H "Authorization: token $TOKEN" "$API/user/repos" | python3 -m json.tool
# 列出指定用户的仓库
curl -s -H "Authorization: token $TOKEN" "$API/users/shumengya/repos" \
| python3 -c "import sys,json; [print(r['name'], r['html_url']) for r in json.load(sys.stdin)]"
# 创建仓库(公开)
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X POST "$API/user/repos" \
-d '{"name":"<仓库名>","description":"描述","private":false}'
# 创建仓库(私有)
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X POST "$API/user/repos" \
-d '{"name":"<仓库名>","description":"描述","private":true}'
# 删除仓库
curl -s -H "Authorization: token $TOKEN" -X DELETE "$API/repos/shumengya/<仓库名>"
# 检查仓库是否存在
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $TOKEN" \
"$API/repos/shumengya/<仓库名>"
# 200=存在, 404=不存在
```
### 推送代码到新仓库
```bash
# 添加远程仓库
git remote add gitea https://git.shumengya.top/shumengya/<仓库名>.git
# 或使用 SSH推荐免输令牌
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/<仓库名>.git
# 推送到 Gitea
git push gitea main
```
### Issue & PR 操作
```bash
# 列出仓库的所有 Issue
curl -s -H "Authorization: token $TOKEN" "$API/repos/shumengya/<仓库名>/issues"
# 创建 Issue
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X POST "$API/repos/shumengya/<仓库名>/issues" \
-d '{"title":"标题","body":"描述内容"}'
# 关闭 Issue编号 42
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X PATCH "$API/repos/shumengya/<仓库名>/issues/42" \
-d '{"state":"closed"}'
# 创建 PR
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X POST "$API/repos/shumengya/<仓库名>/pulls" \
-d '{"title":"标题","head":"feature-branch","base":"main","body":"描述"}'
```
---
## 方式三Python 辅助脚本(零依赖)
纯 Python 标准库,无需 `pip install`,直接运行。
### gitea_api.py — API 直连脚本
```bash
# 列出仓库
python scripts/gitea_api.py repos list
# 创建仓库
python scripts/gitea_api.py repos create sproutclaw --desc "项目描述"
# 删除仓库
python scripts/gitea_api.py repos delete sproutclaw
# 列出 Issue
python scripts/gitea_api.py issues list --repo shumengya/sproutclaw
# 创建 Issue
python scripts/gitea_api.py issues create --repo shumengya/sproutclaw --title "标题" --body "描述"
```
### tea_helper.py — tea CLI 包装脚本
```bash
# 依赖 tea CLI 本身
python scripts/tea_helper.py issues # 列出 Issue
python scripts/tea_helper.py prs # 列出 PR
python scripts/tea_helper.py releases # 列出 Release
python scripts/tea_helper.py close-issue 42 # 关闭 Issue
python scripts/tea_helper.py triage # 按标签分组 Issue
```
---
## 快速工作流示例
### 场景:将本地项目上传到 Gitea基于 sproutclaw 实战)
```bash
# 1. 在 Gitea 上创建仓库
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-X POST "$API/user/repos" \
-d '{"name":"sproutclaw","description":"项目描述","private":false}'
# 2. 添加 Gitea 远程SSH 方式)
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/sproutclaw.git
# 3. 提交本地变更并推送
git add -A
git commit -m "chore: sync to gitea"
git push gitea main
```
### 场景:从零创建并推送
```bash
# 使用 Python 脚本一键创建仓库
python scripts/gitea_api.py repos create <仓库名> --desc "<描述>"
# 添加远程
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/<仓库名>.git
# 推送
git push -u gitea main
```
---
## 补充说明
- **令牌变更**:更新令牌时只需修改 `~/.tea/tea.yml` 中的 `token` 字段,或重新执行 `tea login add`
- **双远程工作流**:项目同时有 GitHub`origin`)和 Gitea`gitea`)远程时,推送时需指定:
```bash
git push origin main # 推送到 GitHub
git push gitea main # 推送到 Gitea
```
- **参考文档**[reference.md](reference.md) | [Gitea API 官方文档](https://docs.gitea.com/api/1.20/)

View File

@@ -0,0 +1,244 @@
# tea CLI Full Command Reference
## Global Flags
| Flag | Short | Description |
|------|-------|-------------|
| `--login` | `-l` | Use a specific configured login |
| `--repo` | `-r` | Override repo as `owner/repo` slug |
| `--remote` | `-R` | Discover login from a git remote name |
| `--output` | `-o` | Output format: `simple`, `table`, `csv`, `tsv`, `yaml`, `json` |
---
## logins / login
| Command | Description |
|---------|-------------|
| `tea login list` | List all configured logins |
| `tea login add` | Add a Gitea instance |
| `tea login edit <name>` | Edit a login entry |
| `tea login delete <name>` | Remove a login |
| `tea login default [name]` | Get or set default login |
| `tea login oauth-refresh` | Refresh OAuth token |
| `tea logout` | Log out from a server |
| `tea whoami` | Show current logged-in user |
### `tea login add` flags
| Flag | Description |
|------|-------------|
| `--url, -u` | Server URL (default: https://gitea.com) |
| `--token, -t` | Access token (Settings > Applications) |
| `--user` | Username for basic auth |
| `--password, --pwd` | Password for basic auth |
| `--oauth, -o` | Interactive OAuth2 flow |
| `--ssh-key, -s` | Path to SSH key |
| `--ssh-agent-key, -a` | SSH public key / fingerprint via agent |
| `--otp` | OTP token |
| `--name, -n` | Login alias name |
| `--insecure, -i` | Disable TLS verification |
| `--no-version-check, --nv` | Skip Gitea version check |
---
## issues / issue / i
### List
```
tea issues list [--state open|closed|all] [--keyword STR] [--labels L1,L2]
[--assignee USER] [--author USER] [--milestone M]
[--limit N] [--page N]
[--fields index,state,kind,author,url,title,body,created,updated,
deadline,assignees,milestone,labels,comments,owner,repo]
```
### Create
```
tea issues create --title TITLE --description DESC
[--labels L1,L2] [--assignees U1,U2]
[--milestone M] [--deadline DATE]
```
### Edit
```
tea issues edit INDEX --title T --description D
--add-labels L1 --remove-labels L2
--add-assignees U1 --milestone M
```
### Other
```
tea issues close INDEX [INDEX...]
tea issues reopen INDEX [INDEX...]
tea issues comment INDEX # Add a comment (interactive)
```
---
## pulls / pull / pr
### List
```
tea pulls list [--state open|closed|all]
[--fields index,state,author,url,title,body,mergeable,base,
base-commit,head,diff,patch,created,updated,deadline,
assignees,milestone,labels,comments,ci]
```
### Create
```
tea pulls create --title T --description D --base BRANCH
[--head BRANCH] [--labels L] [--assignees U]
[--milestone M] [--allow-maintainer-edits]
```
### Review workflow
```
tea pulls checkout 99 # Check out PR locally
tea pulls review 99 # Interactive review
tea pulls approve 99 # Approve (LGTM)
tea pulls reject 99 # Request changes
tea pulls merge 99 # Merge the PR
tea pulls clean 99 # Delete merged branch
```
### Edit
```
tea pulls edit 99 --title T --description D
--add-labels L --remove-labels L
--add-reviewers U1,U2 --remove-reviewers U3
--add-assignees U1 --milestone M
```
### review-comments
```
tea pulls review-comments 99 # List review comments
tea pulls resolve COMMENT_ID # Resolve a review comment
tea pulls unresolve COMMENT_ID
```
---
## repos / repo
```
tea repos list [--owner ORG] [--limit N]
tea repos create --name NAME [--description D] [--private] [--init]
[--default-branch BRANCH] [--template OWNER/REPO]
tea repos fork OWNER/REPO [--organization ORG]
tea repos delete OWNER/REPO
```
---
## releases / release
```
tea releases list [--limit N]
tea releases create --tag TAG --title TITLE --note "Notes"
[--draft] [--prerelease] [--target BRANCH_OR_SHA]
tea releases edit TAG --title T --note N [--draft] [--prerelease]
tea releases delete TAG
tea releases assets list TAG
tea releases assets upload TAG --asset FILE
tea releases assets delete TAG ASSET_ID
```
---
## milestones / milestone
```
tea milestones list
tea milestones create --title T [--description D] [--deadline DATE]
tea milestones edit INDEX --title T
tea milestones close INDEX
tea milestones reopen INDEX
tea milestones delete INDEX
```
---
## labels / label
```
tea labels list
tea labels create --name NAME --color "#HEX" [--description D]
tea labels delete INDEX
```
---
## notifications / notification / n
```
tea notifications list [--all] [--mine] [--limit N]
tea notifications markread [INDEX] # Mark as read
```
---
## admin (server admin only)
```
tea admin users list
tea admin orgs list
```
---
## Output Fields Quick Reference
### Issues `--fields`
`index`, `state`, `kind`, `author`, `author-id`, `url`, `title`, `body`, `created`, `updated`, `deadline`, `assignees`, `milestone`, `labels`, `comments`, `owner`, `repo`
### PRs `--fields`
`index`, `state`, `author`, `author-id`, `url`, `title`, `body`, `mergeable`, `base`, `base-commit`, `head`, `diff`, `patch`, `created`, `updated`, `deadline`, `assignees`, `milestone`, `labels`, `comments`, `ci`
---
## Python Automation Patterns
### Get all open issues as structured data
```python
import subprocess, json
def get_issues(repo=None, state="open"):
cmd = ["tea", "issues", "list", "--state", state, "-o", "json"]
if repo:
cmd += ["-r", repo]
out = subprocess.check_output(cmd, text=True)
return json.loads(out)
```
### Create issue from Python
```python
def create_issue(title, body="", labels=None, repo=None):
cmd = ["tea", "issues", "create", "--title", title]
if body:
cmd += ["--description", body]
if labels:
cmd += ["--labels", ",".join(labels)]
if repo:
cmd += ["-r", repo]
subprocess.run(cmd, check=True)
```
### Bulk close stale issues
```python
issues = get_issues(state="open")
stale = [i for i in issues if "stale" in [l["name"] for l in (i.get("labels") or [])]]
for issue in stale:
idx = issue.get("number") or issue.get("index")
subprocess.run(["tea", "issues", "close", str(idx)], check=True)
print(f"Closed #{idx}")
```
### Export releases to JSON file
```python
import subprocess, json, pathlib
out = subprocess.check_output(["tea", "releases", "list", "-o", "json"], text=True)
pathlib.Path("releases.json").write_text(out)
```

View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""
gitea_api.py — Pure stdlib Gitea API helper for https://git.shumengya.top
Usage:
python scripts/gitea_api.py repos list
python scripts/gitea_api.py repos create <name> [--desc DESC] [--private]
python scripts/gitea_api.py repos delete <name>
python scripts/gitea_api.py repos exists <name>
python scripts/gitea_api.py issues list [--repo owner/name]
python scripts/gitea_api.py issues create --repo owner/name --title TITLE [--body BODY]
Zero external dependencies (stdlib only: urllib, json, argparse, sys, os).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DEFAULT_URL = "https://git.shumengya.top"
DEFAULT_TOKEN = "dbc5dfe55b19e9a32f685a97f126cafc2ef76449"
DEFAULT_OWNER = "shumengya"
# Allow override via environment variables
GITEA_URL = os.environ.get("GITEA_URL", DEFAULT_URL).rstrip("/")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", DEFAULT_TOKEN)
API_BASE = f"{GITEA_URL}/api/v1"
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
def _headers() -> dict[str, str]:
return {
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def _request(
method: str,
path: str,
data: dict[str, Any] | None = None,
) -> tuple[int, Any]:
"""Send HTTP request to Gitea API. Returns (status_code, parsed_json)."""
url = f"{API_BASE}{path}"
body = json.dumps(data).encode("utf-8") if data else None
req = urllib.request.Request(
url,
data=body,
headers=_headers(),
method=method,
)
try:
with urllib.request.urlopen(req) as resp:
raw = resp.read()
status = resp.status
if raw.strip():
return status, json.loads(raw.decode("utf-8"))
return status, None
except urllib.error.HTTPError as e:
try:
detail = json.loads(e.read().decode("utf-8"))
except Exception:
detail = {"message": str(e)}
return e.code, detail
except urllib.error.URLError as e:
print(f"[ERROR] Network error: {e.reason}", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Repository commands
# ---------------------------------------------------------------------------
def cmd_repos_list(_args: argparse.Namespace) -> None:
status, data = _request("GET", f"/users/{DEFAULT_OWNER}/repos?limit=100")
if status != 200:
print(f"[ERROR] Failed to list repos (HTTP {status}): {data}")
sys.exit(1)
if not data:
print("No repositories found.")
return
print(f"{'Name':<30} {'Private':<8} {'Description'}")
print("-" * 80)
for r in sorted(data, key=lambda x: x.get("name", "")):
name = r.get("name", "?")
private = "yes" if r.get("private") else "no"
desc = (r.get("description") or "")[:45]
print(f"{name:<30} {private:<8} {desc}")
def cmd_repos_create(args: argparse.Namespace) -> None:
payload: dict[str, Any] = {
"name": args.name,
"description": args.desc or "",
"private": args.private,
"auto_init": False,
}
status, data = _request("POST", "/user/repos", payload)
if status in (201, 200):
print(f"[OK] Repository created: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}")
print(f" HTTPS: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}.git")
print(f" SSH: ssh://git@{GITEA_URL.split('://')[1]}:8022/{DEFAULT_OWNER}/{args.name}.git")
elif status == 409:
print(f"[WARN] Repository '{args.name}' already exists.", file=sys.stderr)
else:
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
sys.exit(1)
def cmd_repos_delete(args: argparse.Namespace) -> None:
print(f"Deleting repository {DEFAULT_OWNER}/{args.name}...", end=" ")
status, data = _request("DELETE", f"/repos/{DEFAULT_OWNER}/{args.name}")
if status in (204, 200):
print("[OK] Deleted.")
else:
print(f"\n[ERROR] HTTP {status}: {data}", file=sys.stderr)
sys.exit(1)
def cmd_repos_exists(args: argparse.Namespace) -> None:
status, _data = _request("GET", f"/repos/{DEFAULT_OWNER}/{args.name}")
if status == 200:
print(f"[YES] Repository '{args.name}' exists.")
elif status == 404:
print(f"[NO] Repository '{args.name}' does not exist.")
else:
print(f"[UNKNOWN] HTTP {status}", file=sys.stderr)
# ---------------------------------------------------------------------------
# Issue commands
# ---------------------------------------------------------------------------
def cmd_issues_list(args: argparse.Namespace) -> None:
repo = args.repo or f"{DEFAULT_OWNER}/sproutclaw"
status, data = _request("GET", f"/repos/{repo}/issues?state={args.state}&limit=50")
if status != 200:
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
sys.exit(1)
if not data:
print(f"No {args.state} issues in {repo}.")
return
print(f"{'#':<6} {'State':<8} {'Title'}")
print("-" * 70)
for issue in data:
# PRs also appear in issues endpoint; skip them
if issue.get("pull_request"):
continue
num = issue.get("number", "?")
state = issue.get("state", "?")
title = issue.get("title", "?")
print(f"#{num:<5} {state:<8} {title}")
def cmd_issues_create(args: argparse.Namespace) -> None:
if not args.repo:
print("[ERROR] --repo is required (e.g. shumengya/sproutclaw)", file=sys.stderr)
sys.exit(1)
payload: dict[str, Any] = {"title": args.title}
if args.body:
payload["body"] = args.body
status, data = _request("POST", f"/repos/{args.repo}/issues", payload)
if status in (201, 200):
url = data.get("html_url", "?")
print(f"[OK] Issue created: {url}")
else:
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Gitea API helper — 纯 Python 标准库,零外部依赖。"
)
sub = parser.add_subparsers(dest="resource", required=True)
# --- repos ---
repos_parser = sub.add_parser("repos", help="仓库操作")
repos_commands = repos_parser.add_subparsers(dest="action", required=True)
# repos list
repos_commands.add_parser("list", help="列出所有仓库")
# repos create
rc = repos_commands.add_parser("create", help="创建新仓库")
rc.add_argument("name", help="仓库名称")
rc.add_argument("--desc", "-d", default="", help="仓库描述")
rc.add_argument("--private", action="store_true", help="创建私有仓库")
# repos delete
rd = repos_commands.add_parser("delete", help="删除仓库")
rd.add_argument("name", help="仓库名称")
# repos exists
re = repos_commands.add_parser("exists", help="检查仓库是否存在")
re.add_argument("name", help="仓库名称")
# --- issues ---
issues_parser = sub.add_parser("issues", help="Issue 操作")
issues_commands = issues_parser.add_subparsers(dest="action", required=True)
# issues list
il = issues_commands.add_parser("list", help="列出 Issue")
il.add_argument("--repo", "-r", help="仓库路径 (owner/name)")
il.add_argument("--state", default="open", choices=["open", "closed", "all"])
# issues create
ic = issues_commands.add_parser("create", help="创建 Issue")
ic.add_argument("--repo", "-r", required=True, help="仓库路径 (owner/name)")
ic.add_argument("--title", "-t", required=True, help="Issue 标题")
ic.add_argument("--body", "-b", default="", help="Issue 描述")
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
if not GITEA_TOKEN:
print("[ERROR] GITEA_TOKEN environment variable is not set and no default available.")
sys.exit(1)
dispatch = {
("repos", "list"): cmd_repos_list,
("repos", "create"): cmd_repos_create,
("repos", "delete"): cmd_repos_delete,
("repos", "exists"): cmd_repos_exists,
("issues", "list"): cmd_issues_list,
("issues", "create"): cmd_issues_create,
}
key = (args.resource, args.action)
if key in dispatch:
dispatch[key](args)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
tea_helper.py — Python utility for Gitea tea CLI automation.
Usage:
python tea_helper.py issues [--state open|closed|all] [--repo owner/repo]
python tea_helper.py prs [--state open|closed|all] [--repo owner/repo]
python tea_helper.py releases [--repo owner/repo]
python tea_helper.py close-issue <index> [--repo owner/repo]
python tea_helper.py close-pr <index> [--repo owner/repo]
python tea_helper.py create-issue --title TITLE [--desc DESC] [--labels L1,L2]
python tea_helper.py triage [--repo owner/repo]
"""
import argparse
import json
import subprocess
import sys
from typing import Any
# ---------------------------------------------------------------------------
# Core helpers
# ---------------------------------------------------------------------------
def run_tea(*args: str, capture: bool = True) -> subprocess.CompletedProcess:
cmd = ["tea", *args]
result = subprocess.run(cmd, capture_output=capture, text=True)
if result.returncode != 0 and capture:
print(f"[tea error] {result.stderr.strip()}", file=sys.stderr)
return result
def tea_json(*args: str) -> Any:
result = run_tea(*args, "-o", "json")
if result.returncode != 0:
return []
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return []
def repo_flags(repo: str | None) -> list[str]:
return ["-r", repo] if repo else []
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def cmd_issues(args: argparse.Namespace) -> None:
flags = ["issues", "list", "--state", args.state, *repo_flags(args.repo)]
issues = tea_json(*flags)
if not issues:
print("No issues found.")
return
print(f"{'#':<6} {'State':<8} {'Title'}")
print("-" * 60)
for i in issues:
print(f"#{i.get('number', i.get('index','?')):<5} {i.get('state',''):<8} {i.get('title','')}")
def cmd_prs(args: argparse.Namespace) -> None:
flags = ["pulls", "list", "--state", args.state,
"--fields", "index,title,state,author,milestone,updated,labels,ci",
*repo_flags(args.repo)]
prs = tea_json(*flags)
if not prs:
print("No pull requests found.")
return
print(f"{'#':<6} {'State':<8} {'Title'}")
print("-" * 60)
for pr in prs:
print(f"#{pr.get('number', pr.get('index','?')):<5} {pr.get('state',''):<8} {pr.get('title','')}")
def cmd_releases(args: argparse.Namespace) -> None:
flags = ["releases", "list", *repo_flags(args.repo)]
releases = tea_json(*flags)
if not releases:
print("No releases found.")
return
print(f"{'Tag':<20} {'Name':<30} {'Draft':<6} {'Pre':<5}")
print("-" * 65)
for r in releases:
print(f"{r.get('tag_name',''):<20} {r.get('name',''):<30} "
f"{str(r.get('draft', False)):<6} {str(r.get('prerelease', False)):<5}")
def cmd_close_issue(args: argparse.Namespace) -> None:
result = run_tea("issues", "close", str(args.index), *repo_flags(args.repo))
if result.returncode == 0:
print(f"Issue #{args.index} closed.")
def cmd_close_pr(args: argparse.Namespace) -> None:
result = run_tea("pulls", "close", str(args.index), *repo_flags(args.repo))
if result.returncode == 0:
print(f"PR #{args.index} closed.")
def cmd_create_issue(args: argparse.Namespace) -> None:
flags = ["issues", "create", "--title", args.title, *repo_flags(args.repo)]
if args.desc:
flags += ["--description", args.desc]
if args.labels:
flags += ["--labels", args.labels]
result = run_tea(*flags, capture=False)
sys.exit(result.returncode)
def cmd_triage(args: argparse.Namespace) -> None:
"""Group open issues by their first label."""
flags = ["issues", "list", "--state", "open", *repo_flags(args.repo)]
issues = tea_json(*flags)
if not issues:
print("No open issues.")
return
groups: dict[str, list] = {}
for issue in issues:
labels = issue.get("labels") or []
label = labels[0].get("name", "unlabeled") if labels else "unlabeled"
groups.setdefault(label, []).append(issue)
for label, items in sorted(groups.items()):
print(f"\n[{label}] ({len(items)})")
for i in items:
print(f" #{i.get('number', i.get('index','?'))} {i.get('title','')}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="tea CLI helper (Python wrapper)")
p.add_argument("--repo", "-r", help="owner/repo slug (overrides auto-detect)")
sub = p.add_subparsers(dest="command", required=True)
# issues
pi = sub.add_parser("issues", help="List issues")
pi.add_argument("--state", default="open", choices=["open", "closed", "all"])
# prs
pp = sub.add_parser("prs", help="List pull requests")
pp.add_argument("--state", default="open", choices=["open", "closed", "all"])
# releases
sub.add_parser("releases", help="List releases")
# close-issue
pci = sub.add_parser("close-issue", help="Close an issue")
pci.add_argument("index", type=int, help="Issue index/number")
# close-pr
pcp = sub.add_parser("close-pr", help="Close a pull request")
pcp.add_argument("index", type=int, help="PR index/number")
# create-issue
pcr = sub.add_parser("create-issue", help="Create a new issue")
pcr.add_argument("--title", "-t", required=True)
pcr.add_argument("--desc", "-d", default="")
pcr.add_argument("--labels", "-L", default="", help="Comma-separated labels")
# triage
sub.add_parser("triage", help="Group open issues by label")
return p
def main() -> None:
parser = build_parser()
args = parser.parse_args()
dispatch = {
"issues": cmd_issues,
"prs": cmd_prs,
"releases": cmd_releases,
"close-issue": cmd_close_issue,
"close-pr": cmd_close_pr,
"create-issue": cmd_create_issue,
"triage": cmd_triage,
}
dispatch[args.command](args)
if __name__ == "__main__":
main()

1
github-gh-skill/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

4
github-gh-skill/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
*.py[cod]
.venv/
venv/

86
github-gh-skill/SKILL.md Normal file
View File

@@ -0,0 +1,86 @@
---
name: github-gh-skill
description: 使用 GitHub CLI`gh`处理认证、仓库、Issue、Pull Request、Actions、Release以及通过 JSON/JQ 调用 API 查询数据。适用于用户提到 `gh`、GitHub CLI、终端里的 GitHub、PR、Issue、Actions、Release或需要不打开浏览器直接获取 GitHub 数据的场景。
---
# GitHub CLIgh技能
## 何时启用
用户提到:`gh`、GitHub CLI、命令行操作 GitHub、PR/Issue/Release/Actions、或需要从终端拉取 GitHub 信息时,优先按本技能执行。
## 前置检查(一次性)
1. **检查 `gh` 是否已安装**:执行 `gh --version`
- 若命令不存在,**自动安装最新版**:根据系统包管理器执行安装(如 `apt install gh -y``yum install gh -y``brew install gh`)。
- 安装失败再**提示用户手动安装**(参考官网 <https://cli.github.com/>)。
2. 若需私有资源,执行 `gh auth status`;未登录则 `gh auth login`(非 CI 场景由用户完成交互)。
3. 需要脚本化检测时,运行跨平台脚本:`python scripts/check_gh.py`(或 `python3 ...`,仅需 Python 3.8+ 与标准库)。
## 高效原则(给 Agent
- **优先机器可读**:能用 `--json` 就用;需要筛选时用 `--jq`(避免解析人类表格输出)。
- **减少交互**:批量操作用 `--yes` / `-y`;需要合并且非交互时用环境变量或文档中的标志位(见 [reference.md](reference.md))。
- **先定目标再选子命令**issue / pr / repo / workflow / release / api —— 不要猜,缺参数时 `gh <cmd> --help`
- **API 兜底**`gh` 子命令缺能力时用 `gh api`,路径以官方 REST 为准。
## 模块索引(按场景跳转)
| 模块 | 内容 |
|------|------|
| A. 认证与上下文 | `gh auth`,默认仓库 |
| B. 仓库 | `gh repo clone/view/create` |
| C. Issue | `gh issue list/create/view/close` |
| D. PR | `gh pr list/create/view/diff/merge/checkout` |
| E. Actions | `gh run list/view/watch/rerun` |
| F. Release | `gh release list/create` |
| G. 高级 | `gh api`GraphQL |
**详细命令与模板**见 [reference.md](reference.md)。
## A. 认证与上下文
- 查看登录:`gh auth status`
- 设置默认仓库(减少重复 `-R`):在仓库目录内 `gh repo set-default`
## B. 仓库
- 克隆:`gh repo clone owner/name`
- 当前仓库信息JSON`gh repo view --json name,owner,url,defaultBranchRef`
## C. Issue
- 列表JSON`gh issue list --json number,title,state,author,labels`
- 创建:`gh issue create --title "..." --body "..."`
## D. Pull Request
- 列表:`gh pr list --json number,title,state,headRefName`
- 检出本地分支:`gh pr checkout <number>`
- 查看差异:`gh pr diff <number>`
## E. GitHub Actionsworkflow / run
- 运行列表:`gh run list --limit 20`
- 单次运行详情 + 日志:`gh run view <run-id> --log`
## F. Release 与标签
- 列表:`gh release list --limit 20`
- 创建(需明确 tag 与说明):`gh release create <tag> --notes "..."`
## G. `gh api`REST
- GET 示例:`gh api repos/{owner}/{repo}/issues --jq '.[0].title'`
- 占位符:`{owner}` `{repo}` 在已关联 git remote 时常可自动解析;不确定时显式传 `-R owner/repo`
## 错误处理
- **401/403**:检查 `gh auth status`、token 权限repo、workflow 等)。
- **未找到命令**:升级 `gh` 或改用 `gh api`
- **交互阻塞**CI/自动化中避免会打开编辑器的命令;为 `pr merge` 等加 `--merge`/`--squash`/`--rebase` 等非交互选项。
## 附加资源
- 完整命令表与 JSON 字段提示:[reference.md](reference.md)
- 环境检查脚本(模块化,中文注释;跨平台,仅标准库):[scripts/check_gh.py](scripts/check_gh.py)

View File

@@ -0,0 +1,95 @@
# gh 参考手册(渐进披露)
主说明见 [SKILL.md](SKILL.md)。此处补充常用命令与非交互参数。
## 全局
| 用途 | 示例 |
|------|------|
| 帮助 | `gh <command> --help` |
| 指定仓库 | `-R owner/repo` 或在仓库目录执行 |
| JSON 输出 | `--json field1,field2` |
| jq 过滤 | `--jq 'select(.state=="OPEN")'` |
## auth
```bash
gh auth status
gh auth login
gh auth token
```
## repo
```bash
gh repo view --json name,owner,url,description
gh repo clone owner/name
gh repo create name --public --source=. --remote=origin --push
```
## issue
```bash
gh issue list --state open --limit 50
gh issue list --json number,title,labels,assignees
gh issue view 123 --json body,author,comments
gh issue create --title "t" --body "b" --label bug
gh issue close 123
```
## pr
```bash
gh pr list --state open
gh pr view 42 --json title,body,commits,files
gh pr checkout 42
gh pr diff 42
gh pr merge 42 --squash --delete-branch
```
合并时若需避免编辑器:使用 `--title`/`--body` 与明确的合并策略标志。
## workflow / run
```bash
gh workflow list
gh run list --workflow "ci.yml" --limit 10
gh run view <run-id>
gh run watch <run-id>
gh run rerun <run-id>
```
## release
```bash
gh release list
gh release view v1.2.3
gh release create v1.2.3 ./dist/*.zip --notes "..."
```
## api
```bash
# GET
gh api user
# 带查询参数
gh api repos/{owner}/{repo}/pulls --paginate
# POST示例
gh api repos/{owner}/{repo}/issues -f title="x" -f body="y"
```
GraphQL 使用 `gh api graphql -f query='...'`(复杂查询优先查 GitHub 文档)。
## JSON 字段提示
不同子命令可用字段以 `gh <cmd> list --json` 的帮助为准。常见:
- `gh pr list --json`: `number,title,state,headRefName,baseRefName,author,url`
- `gh issue list --json`: `number,title,state,labels,assignees,author`
## CI 环境变量
- `GH_TOKEN`:非交互调用时常用(注意最小权限原则)。
- 具体行为以当前 `gh` 版本文档为准。

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块GitHub CLIgh跨平台环境检测脚本。
说明:跨平台单文件,仅依赖 Python 标准库;
供 Agent 或本地一键验证 gh 是否可用、版本与登录用户(不打印 token
"""
from __future__ import annotations
import shutil
import subprocess
import sys
# 区域:配置常量
GH_SKILL_PREFIX = "github-gh-skill:"
def resolve_gh_executable() -> str:
"""检测 gh 是否在 PATH 中,返回可执行路径。"""
path = shutil.which("gh")
if not path:
print("未找到 gh请先安装https://cli.github.com/", file=sys.stderr)
sys.exit(127)
return path
def get_gh_version_line(gh: str) -> str:
"""获取 gh --version 的首行输出。"""
proc = subprocess.run(
[gh, "--version"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if proc.returncode != 0:
print("gh --version 执行失败", file=sys.stderr)
sys.exit(proc.returncode)
first = (proc.stdout or "").splitlines()
return first[0].strip() if first else ""
def get_github_login(gh: str) -> str | None:
"""
仅查询当前登录用户名gh api避免打印 gh auth status 中的 token 行。
未登录或失败时返回 None。
"""
proc = subprocess.run(
[gh, "api", "user", "--jq", ".login"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if proc.returncode != 0:
return None
login = (proc.stdout or "").strip()
return login or None
def print_auth_summary(gh: str) -> None:
"""输出登录摘要或警告到 stderr。"""
login = get_github_login(gh)
if login:
print(f"{GH_SKILL_PREFIX} 已登录 GitHub 用户: {login}")
else:
print(
"WARNING: gh 未登录或 token 无效,请执行 gh auth login",
file=sys.stderr,
)
def main() -> None:
"""入口:路径、版本、登录状态(不泄露 token"""
gh_path = resolve_gh_executable()
print(f"{GH_SKILL_PREFIX} gh 路径: {gh_path}")
print(f"{GH_SKILL_PREFIX} {get_gh_version_line(gh_path)}")
print_auth_summary(gh_path)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,140 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
---
# Linux SSH Operator
## Overview
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login).
## Workflow
1. Confirm authorization and the target (host, port, user).
2. Prefer SSH keys (recommended) for non-interactive runs; avoid storing passwords in files or chat logs.
3. Start with read-only checks, then apply changes, then verify.
4. If a password prompt or interactive tool is required, run the SSH command in a real terminal/TTY (or enable TTY in your runner).
## Quick Start
### Set up SSH keys (recommended)
Generate a key (ed25519):
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
Install the public key on the server (example uses port 22):
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
Optional: create an alias in `~/.ssh/config`:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
Then connect:
```bash
ssh my-server
```
### Bootstrap alias + passwordless login
When the user provides an alias, host/IP, and password and wants `ssh alias` to work immediately:
1. Write or update the alias with the helper script:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
```
2. If `ssh-copy-id` prompts for a password, run in a TTY and enter the password once.
3. Verify passwordless login:
```bash
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
```
4. Remind the user that `scp` and `rsync` can reuse the same alias:
```bash
scp ./file.txt my-server:/root/
rsync -av ./dir/ my-server:/root/dir/
```
Notes:
- `scripts/ssh_alias_setup.sh` auto-generates `~/.ssh/id_ed25519` if it does not exist.
- It updates `~/.ssh/config` idempotently and defaults to `root`, port `22`, and `StrictHostKeyChecking=accept-new`.
- For tests, set `SSH_CONFIG_FILE=/path/to/temp-config`.
### Run remote commands
- Direct:
```bash
ssh my-server uname -a
```
- With sudo (often needs a TTY):
```bash
ssh -tt my-server sudo systemctl status nginx --no-pager
```
- Via wrapper script (consistent options; supports env defaults like `REMOTE_USER`, `REMOTE_PORT`, `REMOTE_KEY`):
- If you installed this Skill globally in `~/.claude/skills/`, use the absolute paths below (recommended so both Claude Code + OpenCode can find it).
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
```
### Transfer files
Upload:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
Download:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
## Common Ops (snippets)
- Disk: `df -h`, `du -sh /path/* | sort -h`
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
- Services: `systemctl status|restart|stop SERVICE`
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
## Safety
- Never store or paste passwords in repo files or chat logs.
- Avoid `StrictHostKeyChecking=no`; prefer verifying host keys (or use `accept-new` only when appropriate).
- For destructive commands (rm, shutdown, firewall changes), ask for explicit user confirmation and show the exact command first.
## References
- SSH security + troubleshooting: `references/ssh-playbook.md`
## Scripts
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
- `scripts/ssh_run.sh`: run remote commands with consistent options.
- `scripts/ssh_copy.sh`: push/pull files via scp with consistent options.

View File

@@ -0,0 +1,204 @@
---
name: linux-ssh-operator-skill
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件scp/rsync、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
---
# Linux SSH 运维助手(中文说明)
> 本文件是 `SKILL.md` 的中文说明版,便于阅读与维护;技能实际触发仍以 `SKILL.md` 为准。
## 概述
这个 Skill 用于通过 SSH 安全地连接 Linux 服务器,并执行可重复、可验证的远程操作,包括:
- 执行远程命令
- 查看系统与服务日志
- 管理 `systemd` 服务
- 上传和下载文件
- 配置 SSH 别名
- 安装公钥并启用免密登录
- 进行常见运维排障
适合以下场景:
- 登录远程 Linux 服务器
- 在服务器上运行命令
- 查看 `journalctl` / 应用日志
- 启动、停止、重启服务
- 把文件部署到服务器
- 从服务器拉取日志、配置或产物文件
- 按“别名 + IP + 密码”快速配置一键 SSH 免密登录
## 工作流程
1. 先确认授权,以及目标信息:主机、端口、用户名。
2. 优先使用 SSH 密钥进行非交互连接,避免在聊天记录或文件里保存密码。
3. 优先先做只读检查,再执行修改,最后进行验证。
4. 如果需要密码提示或交互式工具,使用真实终端或启用 TTY。
## 快速开始
### 配置 SSH 密钥(推荐)
生成一把 `ed25519` 密钥:
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
把公钥安装到目标服务器(示例端口为 `22`
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
可选:在 `~/.ssh/config` 中配置主机别名:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
之后可直接连接:
```bash
ssh my-server
```
### 一键配置别名 + 免密登录
当用户提供“别名、主机/IP、密码”并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
1. 使用辅助脚本写入或更新 SSH 别名:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
```
2. 如果 `ssh-copy-id` 提示输入密码,请在 TTY 中输入一次密码。
3. 安装完成后验证免密登录:
```bash
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
```
4. 告诉用户 `scp``rsync` 也可以直接复用这个别名:
```bash
scp ./file.txt my-server:/root/
rsync -av ./dir/ my-server:/root/dir/
```
补充说明:
- `scripts/ssh_alias_setup.sh` 在本机没有 `~/.ssh/id_ed25519` 时会自动生成。
- 它会幂等更新 `~/.ssh/config`,默认使用 `root`、端口 `22``StrictHostKeyChecking=accept-new`
- 如果要做临时测试,可以设置 `SSH_CONFIG_FILE=/path/to/temp-config`
### 运行远程命令
直接执行:
```bash
ssh my-server uname -a
```
`sudo` 执行(通常需要 TTY
```bash
ssh -tt my-server sudo systemctl status nginx --no-pager
```
使用封装脚本执行(统一参数风格,支持环境变量默认值如 `REMOTE_USER``REMOTE_PORT``REMOTE_KEY`
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
```
### 传输文件
上传文件:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
```
下载文件:
```bash
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
```
## 常见运维操作
- 磁盘空间:`df -h`
- 目录占用:`du -sh /path/* | sort -h`
- 内存查看:`free -h`
- 进程查看:`ps aux --sort=-%mem | head`
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
- 日志跟踪:`tail -n 200 -f /path/log`
- 服务管理:`systemctl status|restart|stop SERVICE`
- 网络排查:`ss -lntp``ip a``ip r``curl -v http://127.0.0.1:PORT/`
## 安全注意事项
- 不要把密码保存到仓库文件、脚本或聊天记录中。
- 尽量避免使用 `StrictHostKeyChecking=no`
- 首次连接建议核验主机指纹;只有在临时环境自动化场景下,才考虑 `accept-new`
- 对于破坏性命令(如删除、关机、防火墙调整),必须先确认再执行,并展示将要运行的确切命令。
## 附带脚本
### `scripts/ssh_alias_setup.sh`
用于创建或更新 SSH 别名,并可选执行 `ssh-copy-id`,支持:
- 指定别名、主机、用户、端口、私钥
- 自动生成默认的 `~/.ssh/id_ed25519`
- 幂等更新 `~/.ssh/config`
- 可选直接运行 `ssh-copy-id` 安装公钥
- 使用 `--dry-run` 预览即将执行的动作
### `scripts/ssh_run.sh`
用于通过 SSH 执行远程命令,支持:
- 指定用户、端口、私钥
- 设置连接超时
- 启用 TTY
- 使用 `sudo``sudo -n`
- 使用 `--accept-new` 处理首次连接主机
- 使用 `--dry-run` 预览实际将执行的命令
默认可读取这些环境变量:
- `REMOTE_USER`
- `REMOTE_PORT`
- `REMOTE_KEY`
- `REMOTE_CONNECT_TIMEOUT`
### `scripts/ssh_copy.sh`
用于通过 `scp` 上传或下载文件,支持:
- `push` 上传文件到远程服务器
- `pull` 从远程服务器下载文件
- 递归复制目录
- 指定用户、端口、私钥
- 使用 `--accept-new`
- 使用 `--dry-run` 预览命令
默认可读取这些环境变量:
- `REMOTE_USER`
- `REMOTE_PORT`
- `REMOTE_KEY`
## 参考资料
- `references/ssh-playbook.md`SSH 安全建议、常见命令、主机指纹处理与故障排查说明。

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Linux SSH Operator"
short_description: "Operate Linux servers, SSH aliases, and passwordless login"

View File

@@ -0,0 +1,103 @@
# SSH playbook (Linux server ops)
## Defaults and conventions
- Prefer SSH keys (ed25519) and `~/.ssh/config` aliases for repeatable runs.
- Avoid putting passwords in files, prompts, or chat logs. If password auth is required, use an interactive terminal/TTY.
- Start with read-only inspection, then apply changes, then verify.
Recommended env vars for wrappers:
- `REMOTE_USER`: default SSH user
- `REMOTE_PORT`: default SSH port (usually 22)
- `REMOTE_KEY`: path to identity file (private key)
- `REMOTE_CONNECT_TIMEOUT`: connect timeout seconds
## SSH key setup (recommended)
Generate a new key:
```bash
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
```
Copy the public key to the server:
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
```
Add a host alias:
```sshconfig
Host my-server
HostName SERVER_IP
Port 22
User USER
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
```
## Common tasks
### Connectivity and OS info
```bash
ssh my-server "whoami && hostname && uname -a"
ssh my-server "cat /etc/os-release"
```
### Disk and memory
```bash
ssh my-server "df -h"
ssh my-server "free -h"
ssh my-server "du -sh /var/log/* | sort -h | tail"
```
### Processes and ports
```bash
ssh my-server "ps aux --sort=-%mem | head"
ssh my-server "ss -lntp"
```
### Logs (systemd)
```bash
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
ssh my-server "journalctl -u SERVICE -f --no-pager"
```
### Services (systemd)
Status:
```bash
ssh my-server "systemctl status SERVICE --no-pager"
```
Restart (often needs sudo and TTY):
```bash
ssh -tt my-server "sudo systemctl restart SERVICE"
```
Non-interactive sudo (fails if a password prompt would be required):
```bash
ssh my-server "sudo -n systemctl restart SERVICE"
```
## Safer host key handling
- Prefer verifying the host key fingerprint out-of-band on first connect.
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
## Troubleshooting quick hits
- `Permission denied (publickey)`: wrong user, wrong key, server missing your public key, or `sshd` settings.
- `Connection timed out`: routing/firewall/security group, wrong port, server down.
- `No route to host`: network path missing (VPN, subnet, ACL).

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Configure an SSH alias and optionally install the current public key for passwordless login.
Usage:
ssh_alias_setup.sh [options] ALIAS HOST
Options:
-u, --user USER SSH user (default: root)
-p, --port PORT SSH port (default: 22)
-i, --key PATH Identity file (default: ~/.ssh/id_ed25519)
--install-key Run ssh-copy-id after writing the alias
--no-accept-new Disable StrictHostKeyChecking=accept-new
--dry-run Print planned actions without changing files
-h, --help Show help
Environment:
SSH_CONFIG_FILE Override SSH config path for testing
REMOTE_KEY Default key path
REMOTE_PORT Default SSH port
Examples:
ssh_alias_setup.sh bigmengya 192.168.1.233 --install-key
ssh_alias_setup.sh -u ubuntu -p 2222 devbox 10.0.0.8
USAGE
}
expand_path() {
local value="$1"
case "$value" in
~) printf '%s\n' "$HOME" ;;
~/*) printf '%s/%s\n' "$HOME" "${value#~/}" ;;
*) printf '%s\n' "$value" ;;
esac
}
user="root"
port="${REMOTE_PORT:-22}"
key="${REMOTE_KEY:-$HOME/.ssh/id_ed25519}"
accept_new=true
install_key=false
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
--install-key)
install_key=true
shift
;;
--no-accept-new)
accept_new=false
shift
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
if [[ $# -lt 2 ]]; then
usage >&2
exit 2
fi
alias_name="$1"
host="$2"
key="$(expand_path "$key")"
config_file="$(expand_path "${SSH_CONFIG_FILE:-$HOME/.ssh/config}")"
ssh_dir="$(dirname "$config_file")"
key_pub="${key}.pub"
key_dir="$(dirname "$key")"
if $dry_run; then
echo "Would ensure directory: $ssh_dir"
echo "Would ensure key directory: $key_dir"
else
mkdir -p "$ssh_dir" "$key_dir"
chmod 700 "$ssh_dir" "$key_dir"
fi
if [[ ! -f "$key" ]]; then
if $dry_run; then
printf 'Would generate key: ssh-keygen -t ed25519 -C %q -f %q -N %q\n' "codex" "$key" ""
else
ssh-keygen -t ed25519 -C "codex" -f "$key" -N ""
fi
fi
if [[ ! -f "$key_pub" ]]; then
echo "Public key not found: $key_pub" >&2
exit 1
fi
if $dry_run; then
cat <<DRYRUN
Would write/update SSH alias:
Host $alias_name
HostName $host
User $user
Port $port
ServerAliveInterval 30
ServerAliveCountMax 3
ConnectTimeout 10
StrictHostKeyChecking $( $accept_new && printf 'accept-new' || printf 'ask' )
IdentityFile $key
IdentitiesOnly yes
DRYRUN
else
python3 - "$config_file" "$alias_name" "$host" "$user" "$port" "$key" "$accept_new" <<'PY'
from pathlib import Path
import sys
config_path = Path(sys.argv[1])
alias_name = sys.argv[2]
host = sys.argv[3]
user = sys.argv[4]
port = sys.argv[5]
key = sys.argv[6]
accept_new = sys.argv[7].lower() == 'true'
existing = config_path.read_text(encoding='utf-8') if config_path.exists() else ''
lines = existing.splitlines()
out: list[str] = []
skip = False
for line in lines:
stripped = line.strip()
if stripped.lower().startswith('host '):
host_names = stripped[5:].split()
if alias_name in host_names:
skip = True
continue
skip = False
out.append(line)
continue
if skip:
continue
out.append(line)
block = [
f'Host {alias_name}',
f' HostName {host}',
f' User {user}',
f' Port {port}',
' ServerAliveInterval 30',
' ServerAliveCountMax 3',
' ConnectTimeout 10',
f' StrictHostKeyChecking {"accept-new" if accept_new else "ask"}',
f' IdentityFile {key}',
' IdentitiesOnly yes',
]
text = '\n'.join(out).strip()
if text:
text += '\n\n'
text += '\n'.join(block) + '\n'
config_path.write_text(text, encoding='utf-8')
PY
chmod 600 "$config_file"
fi
printf 'Alias ready: ssh %s\n' "$alias_name"
printf 'Config file: %s\n' "$config_file"
if $install_key; then
if ! command -v ssh-copy-id >/dev/null 2>&1; then
echo 'ssh-copy-id is required but not installed.' >&2
exit 1
fi
if ! [ -t 0 ]; then
echo 'Warning: no TTY detected; password prompt may fail. Prefer running with a terminal/TTY.' >&2
fi
if $dry_run; then
printf 'Would run: ssh-copy-id -i %q %q\n' "$key_pub" "$alias_name"
else
ssh-copy-id -i "$key_pub" "$alias_name"
fi
fi

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Copy files via scp with consistent options.
Usage:
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
ssh_copy.sh [options] pull HOST REMOTE_PATH LOCAL_PATH
Options:
-u, --user USER Override SSH user (or set REMOTE_USER)
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-r, --recursive Copy directories recursively
--accept-new Set StrictHostKeyChecking=accept-new
--dry-run Print the scp command that would run
-h, --help Show help
Environment defaults:
REMOTE_USER, REMOTE_PORT, REMOTE_KEY
Examples:
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
USAGE
}
port="${REMOTE_PORT:-22}"
user="${REMOTE_USER:-}"
key="${REMOTE_KEY:-}"
recursive=false
accept_new=false
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
-r|--recursive)
recursive=true
shift
;;
--accept-new)
accept_new=true
shift
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
if [[ $# -lt 4 ]]; then
usage >&2
exit 2
fi
direction="$1"
shift
host="$1"
shift
dest_host="$host"
if [[ -n "$user" ]]; then
host_no_user="${host#*@}"
dest_host="${user}@${host_no_user}"
fi
scp_opts=(-P "$port" -p)
if [[ -n "$key" ]]; then
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $recursive; then
scp_opts+=(-r)
fi
if $accept_new; then
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
case "$direction" in
push)
local_path="$1"
remote_path="$2"
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
;;
pull)
remote_path="$1"
local_path="$2"
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
;;
*)
echo "Unknown direction: $direction (expected: push|pull)" >&2
usage >&2
exit 2
;;
esac
if $dry_run; then
printf '%q ' "${full_cmd[@]}"
printf '\n'
exit 0
fi
"${full_cmd[@]}"

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Run a remote command over SSH with consistent, script-friendly options.
Usage:
ssh_run.sh [options] HOST -- COMMAND [ARG...]
ssh_run.sh [options] HOST # interactive shell
Options:
-u, --user USER Override SSH user (or set REMOTE_USER)
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
-i, --key PATH Identity file (default: REMOTE_KEY)
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
--accept-new Set StrictHostKeyChecking=accept-new
--sudo Prefix command with sudo --
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
--connect-timeout SEC Connect timeout (default: REMOTE_CONNECT_TIMEOUT or 10)
--dry-run Print the ssh command that would run
-h, --help Show help
Environment defaults:
REMOTE_USER, REMOTE_PORT, REMOTE_KEY, REMOTE_CONNECT_TIMEOUT
Examples:
ssh_run.sh --user ubuntu 10.0.0.1 -- uname -a
ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
USAGE
}
port="${REMOTE_PORT:-22}"
user="${REMOTE_USER:-}"
key="${REMOTE_KEY:-}"
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
tty=false
accept_new=false
sudo_mode=""
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--user)
user="${2:-}"
shift 2
;;
-p|--port)
port="${2:-}"
shift 2
;;
-i|--key)
key="${2:-}"
shift 2
;;
-t|--tty)
tty=true
shift
;;
--accept-new)
accept_new=true
shift
;;
--sudo)
sudo_mode="sudo"
shift
;;
--sudo-non-interactive)
sudo_mode="sudo-n"
shift
;;
--connect-timeout)
connect_timeout="${2:-}"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
host="$1"
shift
dest="$host"
if [[ -n "$user" ]]; then
host_no_user="${host#*@}"
dest="${user}@${host_no_user}"
fi
ssh_opts=(
-p "$port"
-o "ConnectTimeout=${connect_timeout}"
-o "ServerAliveInterval=30"
-o "ServerAliveCountMax=3"
)
if [[ -n "$key" ]]; then
ssh_opts+=(-i "$key" -o "IdentitiesOnly=yes")
fi
if $accept_new; then
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
fi
if $tty; then
ssh_opts+=(-tt)
fi
cmd=("$@")
if [[ ${#cmd[@]} -gt 0 && "${cmd[0]}" == "--" ]]; then
cmd=("${cmd[@]:1}")
fi
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
if [[ "$sudo_mode" == "sudo-n" ]]; then
cmd=("sudo" "-n" "--" "${cmd[@]}")
else
cmd=("sudo" "--" "${cmd[@]}")
fi
fi
full_cmd=(ssh "${ssh_opts[@]}" "$dest")
if [[ ${#cmd[@]} -gt 0 ]]; then
full_cmd+=("${cmd[@]}")
fi
if $dry_run; then
printf '%q ' "${full_cmd[@]}"
printf '\n'
exit 0
fi
"${full_cmd[@]}"

View File

@@ -0,0 +1,39 @@
---
name: mengya-mail-skill
description: 使用萌芽邮箱 API 发送邮件、列出邮件、读取邮件或测试邮箱连通性。优先用于脚本化收发件和自动化通知。
---
# 萌芽邮箱 Skill
优先使用下面这个本地脚本:
```bash
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py --help
```
## 常用场景
- `send-email`:发送邮件
- `send-template`:发送模板邮件
- `test-connection`:检查 SMTP / IMAP 连通性
- `list-emails`:列出邮件
- `read-email`:读取邮件
- `templates`:查看可用模板
## 示例
```bash
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py test-connection
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py send-email --to mail@smyhub.com --subject "问候" --text-body "你好,我是树萌芽。" --from-name "通知"
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py list-emails --criteria UNSEEN --limit 10
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py read-email --uid 12345
python3 /shumengya/project/skills/mengya-mail-skill/scripts/mengya-mail-api.py send-template --template birthday --to mail@example.com --var name=小王 --var sender=树萌芽 --from-name "生日通知"
```
## 说明
- 脚本默认读取 `/shumengya/project/python/mengya-mail-api/.env`
- 真正的业务实现放在 `/shumengya/project/python/mengya-mail-api/src/mengya_mail_api/`skill 目录里的脚本只是入口
- 发送邮件时优先直接传 `--to``--subject``--text-body`
- 结果默认输出 JSON便于后续处理
- 如果只是给人看,可以加 `--format md`

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from pathlib import Path
MAIL_SRC = Path("/shumengya/project/python/mengya-mail-api/src")
if str(MAIL_SRC) not in sys.path:
sys.path.insert(0, str(MAIL_SRC))
from mengya_mail_api.cli import main # noqa: E402
if __name__ == "__main__":
raise SystemExit(main())

202
mengya-rag-skill/SKILL.md Normal file
View File

@@ -0,0 +1,202 @@
---
name: mengya-rag
description: 萌芽本地 RAG 知识库,基于 LlamaIndex + DeepSeek + BM25/向量混合检索的 Obsidian 笔记问答工具。当用户提到查笔记、查知识库、根据笔记回答问题、搜索自己的文档、翻看笔记内容、查自己的博客文章、查教程、查项目文档、整理笔记素材、问"我写过什么""我之前记录过什么""我的笔记里有没有关于X的内容"时,一定要使用本技能。也适用于用户需要查询知识库状态、同步笔记、重建索引的场景。该技能通过 CLI 工具 mengya-rag 实现,项目路径 /shumengya/project/agent/mengya-rag。
---
# 萌芽 RAG 知识库
基于 LlamaIndex + DeepSeek + BM25/本地向量混合检索的个人 Obsidian 笔记问答系统。
**当前状态:** 已索引 495 篇 Markdown 笔记,共计 1802 个语义分块,覆盖 Docker、Linux、Android、AI Agent、博客文章、工具教程等主题。
## 前置检查
执行任何操作前,先确认工具可用:
```bash
cd /shumengya/project/agent/mengya-rag && uv run mengya-rag status --json
```
返回示例:
```json
{"ok": true, "markdown_note_count": 495, "index_node_count": 1802, "db_exists": true, "deepseek_model": "deepseek-v4-flash"}
```
如果 `ok: false``db_exists: false`,需要先执行 `reindex` 重建索引后再继续。
## 项目路径
```text
项目根目录: /shumengya/project/agent/mengya-rag
笔记目录: data/notes/ (495 篇 Markdown 笔记)
索引目录: data/index/bm25/ (SQLite + sqlite-vec 向量库)
配置: .env (DeepSeek API Key)
```
## 技术栈
| 组件 | 技术 |
|------|------|
| RAG 框架 | LlamaIndex |
| 本地 embedding | BAAI/bge-small-zh-v1.5 (FastEmbed + ONNX无 PyTorch/GPU 依赖) |
| 大模型回答 | DeepSeek API (默认 deepseek-v4-flash) |
| 检索方式 | BM25 关键词 + 向量语义 混合检索 + RRF 融合 + 同文件去重 |
| 向量库 | SQLite + sqlite-vec单文件无外部服务依赖|
| 运行环境 | Python 3.11+, uv 包管理 |
## CLI 命令参考
所有命令从项目根目录执行,`uv run` 前缀:
### 核心命令(推荐 Agent 统一使用 `--json`
| 命令 | 用途 | 推荐场景 |
|------|------|---------|
| `mengya-rag status --json` | 检查索引和配置状态 | 对话开始时先跑一次确认环境 |
| `mengya-rag search "Q" -k N --json` | 纯检索,不调大模型 | 需要自己分析检索结果时 |
| `mengya-rag context "Q" -k N --json` | 返回拼接好的 context 字符串 | 需要把检索结果注入其他大模型提示词时 |
| `mengya-rag read "path" --json` | 读取完整 Markdown 原文 | 检索命中后需要补充上下文时 |
| `mengya-rag ask "Q" -k N --json` | RAG + DeepSeek 完整问答 | 用户直接想问笔记里的内容时 |
| `mengya-rag reindex --json` | 同步笔记 + 重建索引 | 笔记更新后 |
### 常用参数
```bash
-k, --top-k <num> # 返回条数(默认 6
--mode auto # 自动判断:普通问答或盘点列表(默认)
--mode hybrid # 强制语义检索模式
--mode inventory # 强制盘点/目录检索模式
--env-file /path/.env # 指定配置文件
--notes-dir /path # 覆盖笔记目录
--index-dir /path # 覆盖索引目录
```
## Agent 工作流
当用户提到查笔记、查知识库、找文档时,按以下顺序操作:
### 第一步:检查状态
```bash
cd /shumengya/project/agent/mengya-rag && uv run mengya-rag status --json
```
确认 `ok: true``db_exists: true``markdown_note_count > 0`
### 第二步:根据用户问题类型选择命令
**场景 A用户问"关于X的内容是什么""XX怎么做"**
→ 使用 `ask`,让 RAG 完成检索 + 回答
```bash
uv run mengya-rag ask "用户的问题" -k 4 --json
```
解析返回的 `answer``sources`,直接呈现给用户。
**场景 B用户想查"有哪些笔记""我写过什么"**
→ 检索模式会自动切换到盘点模式,用 `search` 即可
```bash
uv run mengya-rag search "博客文章" -k 10 --json
```
返回结果中的 `results[].title``results[].heading_path` 可以直接展示给用户。
**场景 C检索结果不够详细需要看完整原文**
→ 先用 `search` 拿到 `source` 文件名,再用 `read` 读完整内容
```bash
# 先检索
uv run mengya-rag search "Docker 网络配置" -k 3 --json
# 取出结果中的 source 字段(如 "Docker/Docker网络配置.md")后执行:
uv run mengya-rag read "Docker/Docker网络配置.md" --json
```
**场景 D需要把笔记内容作为上下文给另一个大模型**
→ 使用 `context` 模式,返回纯文本 context
```bash
uv run mengya-rag context "WireGuard 配置方法" -k 4 --json
```
返回的 `context` 字段可直接拼入其他模型的提示词中。
**场景 E用户说笔记更新了/查不到新内容**
→ 执行同步 + 重建索引
```bash
uv run mengya-rag reindex --json
```
### 第三步:呈现结果
- **ask 结果**:直接展示 `answer` 内容,并附上 `sources` 来源说明
- **search 结果**:列表展示文件名和标题路径,询问用户是否需要进一步阅读某篇
- **read 结果**:展示 `content`,如果内容较长则先总结再给用户
- **context 结果**:说明已获取上下文,询问用户想用它做什么
## 检索模式说明
系统自动识别两类问题,无需手动指定(也可通过 `--mode` 强制):
| 问题类型 | 示例 | 检索策略 |
|---------|------|---------|
| 普通问答 | "Docker 常用命令有哪些""WireGuard 怎么配置" | Markdown 结构感知分块 → BM25 + 向量混合检索 → RRF 融合 → DeepSeek 生成回答 |
| 盘点列表 | "我有哪些博客文章""安卓 Gradle 相关笔记" | 文件级摘要节点 → BM25 检索 → 关键词覆盖过滤 → 输出文件列表和预览 |
## 查询模式示例
**普通问答:**
```
用户 > docker-compose 怎么设置资源限制
Agent > uv run mengya-rag ask "docker-compose 资源限制配置" -k 4 --json
返回 > answer: "在 docker-compose.yml 中通过 deploy.resources.limits 设置..."
> sources: ["Docker/Docker Compose配置.md", "Docker/资源限制.md"]
Agent > 展示回答内容和来源
```
**盘点列表:**
```
用户 > 我有哪些博客文章
Agent > uv run mengya-rag search "博客文章" -k 10 --json
返回 > results[].title 列表
Agent > 展示文件列表,询问用户想读哪篇
```
## 维护操作
```bash
# 同步笔记 + 重建索引(一步完成)
uv run mengya-rag reindex --json
# 或分步执行:
uv run mengya-sync-notes # 从 bigmengya 同步最新笔记
uv run mengya-build-index # 重建索引
```
## 错误处理
| 现象 | 可能原因 | 处理方式 |
|------|---------|---------|
| `db_exists: false` | 索引未构建 | 执行 `uv run mengya-rag reindex --json` |
| `ok: false` | 项目或配置问题 | 检查 `.env``DEEPSEEK_API_KEY` 是否配置 |
| `command not found: uv` | uv 未安装 | `pip install uv``curl -LsSf https://astral.sh/uv/install.sh | sh` |
| `markdown_note_count: 0` | 笔记目录为空 | 执行 `uv run mengya-rag reindex --json` 从 bigmengya 同步 |
| DeepSeek API 报错 | API Key 无效或网络问题 | 检查 `.env` 中的 `DEEPSEEK_API_KEY` 和网络连通性 |
## 配置参考
`.env` 文件中的可调参数:
```env
DEEPSEEK_API_KEY=你的API密钥 # 必填
TOP_K=6 # 默认检索返回条数
EMBED_MODEL=BAAI/bge-small-zh-v1.5 # 本地 embedding 模型
EMBED_BATCH_SIZE=32 # embedding 批处理大小
VECTOR_WEIGHT=0.65 # 向量检索权重(混合模式)
BM25_WEIGHT=0.35 # BM25 关键词权重
```
## 参考
- 完整文档:[README.md](/shumengya/project/agent/mengya-rag/README.md)
- JSON 输出格式参考:[references/output-schema.md](references/output-schema.md)
- 源码目录:`/shumengya/project/agent/mengya-rag/src/mengya_rag/`

View File

@@ -0,0 +1,105 @@
# mengya-rag JSON 输出格式参考
所有命令加 `--json` 后输出 JSON供 Agent 程序化解析。
## status
```json
{
"ok": true,
"command": "status",
"project_root": "...",
"notes_dir": "...",
"index_dir": "...",
"db_exists": true,
"nodes_file_exists": true,
"deepseek_api_key_configured": true,
"deepseek_model": "deepseek-v4-flash",
"markdown_note_count": 495,
"index_node_count": 1802
}
```
## search
```json
{
"ok": true,
"command": "search",
"question": "用户问题",
"mode": "hybrid",
"top_k": 2,
"results": [
{
"source": "Docker/Docker常用命令总结.md",
"title": "Docker常用命令总结",
"heading_path": "容器生命周期管理",
"chunk_index": 0,
"score": 0.85,
"raw_score": 0.75,
"metadata": { "h1": "Docker常用命令总结", "h2": "容器生命周期管理" },
"content": "## 容器操作\n```bash\ndocker run ...\n```"
}
]
}
```
## context
```json
{
"ok": true,
"command": "context",
"question": "用户问题",
"mode": "hybrid",
"top_k": 2,
"context": "[1] 来源: ...\n文件: Docker/xxx.md\n标题: XXX\n标题路径: ...\n\n---\n笔记内容...\n\n[2] 来源: ...\n...",
"sources": ["Docker/xxx.md", "Docker/yyy.md"]
}
```
## read
```json
{
"ok": true,
"command": "read",
"source": "Docker/Docker常用命令总结.md",
"content": "## 完整 Markdown 原文内容..."
}
```
## ask
```json
{
"ok": true,
"command": "ask",
"question": "用户问题",
"mode": "hybrid",
"top_k": 4,
"answer": "DeepSeek 生成的回答文本...",
"sources": ["文件1.md", "文件2.md"]
}
```
## reindex
```json
{
"ok": true,
"command": "reindex",
"synced_notes": 495,
"indexed_nodes": 1802,
"duration_seconds": 12.5
}
```
## 错误返回
```json
{
"ok": false,
"error": "错误描述信息"
}
```

114
quark-sign-skill/SKILL.md Normal file
View File

@@ -0,0 +1,114 @@
---
name: quark-sign-skill
description: 夸克网盘自动签到。支持通过 URL 参数执行每日签到获取容量奖励零外部依赖Python 标准库即可运行)。
---
# 夸克网盘签到
自动执行夸克网盘每日签到,获取容量奖励。
## 触发场景
- 用户提到「夸克签到」「夸克网盘签到」「签到」且上下文明确指向夸克网盘
- 用户要求配置夸克网盘自动签到
## 前置要求
- Python 3.7+(已预装)
- 夸克网盘签到 URL从夸克 App 或网页获取)
## 获取签到 URL
1. 打开夸克网盘 App 或网页版
2. 进入「容量管理」或「签到」页面
3. 浏览器开发者工具 → Network → 找到类似 `growth/sign``growth/reward_record` 的请求
4. 复制完整 URL包含 `kps``sign``vcode` 参数)
URL 格式示例:
```
https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=xxx&sign=yyy&vcode=zzz
```
## 使用方式
### 方式一:直接运行(推荐)
如果 skill 目录下已配置了默认 `config.json`,无需任何参数即可运行:
```bash
python scripts/quark_sign.py
```
### 方式二:直接传 URL
```bash
python scripts/quark_sign.py --url "你的完整URL"
```
### 方式三:配置文件
```bash
# 1. 复制示例配置并编辑
cp config.json.example ~/.config/quark_sign.json
# 填入你的 URL
# 2. 运行
python scripts/quark_sign.py --config ~/.config/quark_sign.json
```
### 方式四:环境变量
```bash
export QUARK_SIGN_URL="你的完整URL"
python scripts/quark_sign.py
```
## 常用命令
```bash
# 执行签到
python scripts/quark_sign.py --url "URL"
# 仅检查今日是否已签到
python scripts/quark_sign.py --url "URL" --check
# 查看签到信息
python scripts/quark_sign.py --url "URL" --info
```
## 输出示例
```
🚀 执行签到...
✅ 签到成功!获得 100.00 MB
```
```
🚀 执行签到...
今日已签到过,无需重复签到
```
## 默认配置
skill 目录下已内置 `config.json`,作为默认 URL 来源。URL 查找优先级:
1. `--url` 参数
2. `--config` 指定的配置文件
3. 环境变量 `QUARK_SIGN_URL`
4. skill 内置 `config.json`(默认使用)
## 注意事项
- URL 中的 `kps``sign` 参数有有效期,如失效需重新获取
- 签到奖励通常为 100MB~1GB 不等,随机发放
- 脚本支持 `requests` 库(如已安装)或纯 Python 标准库(零依赖)
- 默认配置适合个人使用,多用户场景建议各自使用 `--config``--url`
## 配置 cron 定时任务
```bash
# 每天 9 点自动签到
crontab -e
# 添加:
0 9 * * * python3 /path/to/quark-sign-skill/scripts/quark_sign.py --config /root/.config/quark_sign.json >> /var/log/quark_sign.log 2>&1
```

View File

@@ -0,0 +1,3 @@
{
"url": "https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=Tj6VYdauGenq2rmPE33ohRAtDymhNFX6jKshCB38E3zVZiDAcGttvmkoAgTNU4n%252B8d7JiORQFKb%252F%252BEU524TsTfP1zN4ZWMXH5UMsxTD%252BLuW%252F8w%253D%253D&sign=Tj7N%252BNUA%252ByXE53C%252FPtR0xBpqepeIHSWt9RtzS66kyDIp9MNlfHO2OJ7zvOZQeK5QuAg%253D&vcode=1780291057288&_size=10&uc_param_str=dnfrpfbivessbtbmnilauputogpintnwmtsvcppcprsnnnchmicckp&fr=android&pf=3300&bi=37280&ve=10.2.3.104&ss=411x833&la=zh&ut=Tj5VTPsW0B0jX%2BOb3U5q%2BcX97R%2Bo%2FcYuH5nOtiswJy9O9Q%3D%3D&nt=6&nw=5G&mt=qUMBdVdLPFs5VQKefOxlxCMt4YMRf%2FRe&sv=release&pr=qk_clouddrive&ch=kkcloud%40store_xiaomi&mi=22127RK46C&kp=Tj6VYdauGenq2rmPE33ohRAtDymhNFX6jKshCB38E3zVZiDAcGttvmkoAgTNU4n%2B8d7JiORQFKb%2F%2BEU524TsTfP1zN4ZWMXH5UMsxTD%2BLuW%2F8w%3D%3D"
}

View File

@@ -0,0 +1,3 @@
{
"url": "https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=...&sign=...&vcode=..."
}

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
夸克网盘自动签到脚本
用法:
python quark_sign.py --url "你的夸克URL" [--check]
python quark_sign.py --config ~/.config/quark_sign.json
"""
import argparse
import json
import os
import sys
from urllib.parse import urlparse, parse_qs, unquote
# 尝试导入 requests如果没有则使用 urllib
HAS_REQUESTS = True
try:
import requests
except ImportError:
HAS_REQUESTS = False
import urllib.request
import urllib.parse
import urllib.error
import ssl
class QuarkSign:
BASE_URL = "https://drive-m.quark.cn/1/clouddrive/capacity/growth"
def __init__(self, url: str):
self.params = self._extract_params(url)
if not self.params.get("kps") or not self.params.get("sign"):
raise ValueError("URL 中缺少 kps 或 sign 参数,请检查 URL 是否完整")
def _extract_params(self, url: str) -> dict:
parsed = urlparse(url)
params = parse_qs(parsed.query)
return {k: unquote(v[0]) for k, v in params.items()}
def _build_query(self, extra: dict = None) -> dict:
q = {
"pr": "ucpro",
"fr": "android",
"kps": self.params.get("kps"),
"sign": self.params.get("sign"),
"vcode": self.params.get("vcode", "1780291057288"),
}
if extra:
q.update(extra)
return q
def _post(self, endpoint: str, data: dict = None, params: dict = None) -> dict:
url = f"{self.BASE_URL}/{endpoint}"
query = self._build_query(params)
if HAS_REQUESTS:
resp = requests.post(url, json=data or {}, params=query, timeout=10)
return resp.json()
else:
# 使用 urllib fallback
full_url = url + "?" + urllib.parse.urlencode(query)
payload = json.dumps(data or {}).encode("utf-8")
req = urllib.request.Request(
full_url,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
ctx = ssl.create_default_context()
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
def _get(self, endpoint: str, params: dict = None) -> dict:
url = f"{self.BASE_URL}/{endpoint}"
query = self._build_query(params)
if HAS_REQUESTS:
resp = requests.get(url, params=query, timeout=10)
return resp.json()
else:
full_url = url + "?" + urllib.parse.urlencode(query)
req = urllib.request.Request(full_url, method="GET")
ctx = ssl.create_default_context()
with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8"))
def sign(self) -> dict:
"""执行签到"""
return self._post("sign", data={"sign_cyclic": True})
def info(self) -> dict:
"""获取签到信息"""
return self._get("sign", params={"_size": 1})
def reward_info(self) -> dict:
"""获取奖励记录"""
return self._get("reward_record", params={"_size": 10})
def format_reward(reward_bytes: int) -> str:
if reward_bytes >= 1024 ** 3:
return f"{reward_bytes / (1024 ** 3):.2f} GB"
return f"{reward_bytes / (1024 ** 2):.2f} MB"
def main():
parser = argparse.ArgumentParser(description="夸克网盘自动签到")
parser.add_argument("--url", help="夸克网盘分享/签到 URL包含 kps 和 sign 参数)")
parser.add_argument("--config", "-c", help="配置文件路径JSON 格式)")
parser.add_argument("--check", action="store_true", help="仅检查签到状态,不执行签到")
parser.add_argument("--info", action="store_true", help="显示签到信息")
args = parser.parse_args()
# 获取 URL
url = args.url
if not url and args.config:
with open(os.path.expanduser(args.config)) as f:
cfg = json.load(f)
url = cfg.get("url")
if not url:
# 尝试环境变量
url = os.environ.get("QUARK_SIGN_URL")
if not url:
# 尝试读取默认配置skill 内置)
script_dir = os.path.dirname(os.path.abspath(__file__))
default_cfg = os.path.join(script_dir, "..", "config.json")
if os.path.exists(default_cfg):
with open(default_cfg) as f:
cfg = json.load(f)
url = cfg.get("url")
if not url:
print("❌ 请提供 URL--url、--config 或环境变量 QUARK_SIGN_URL")
print(" 提示:可在 skill 目录下创建 config.json 写入默认 URL")
sys.exit(1)
try:
qs = QuarkSign(url)
except ValueError as e:
print(f"{e}")
sys.exit(1)
if args.info:
print("📋 获取签到信息...")
info = qs.info()
print(json.dumps(info, indent=2, ensure_ascii=False))
return
if args.check:
print("🔍 检查签到状态...")
info = qs.info()
data = info.get("data", {})
if data.get("sign_daily"):
print("✅ 今日已签到")
else:
print("⏳ 今日未签到")
return
# 执行签到
print("🚀 执行签到...")
result = qs.sign()
status = result.get("status")
code = result.get("code")
msg = result.get("message", "")
data = result.get("data", {})
if data and status == 200:
reward = data.get("sign_daily_reward", 0)
print(f"✅ 签到成功!获得 {format_reward(reward)}")
elif "repeat" in msg.lower() or code == 44210:
print(" 今日已签到过,无需重复签到")
else:
print(f"❌ 签到失败: {msg} (code={code}, status={status})")
sys.exit(1)
if __name__ == "__main__":
main()