commit dea5db60d917ab1c2031f1efb789dca185cb3e7a Author: shumengya Date: Fri Jun 12 17:48:53 2026 +0800 Initial commit: add all skills Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0cabee --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +*.pyo +.env +*.log diff --git a/cli-docs-skills/cli-docs/SKILL.md b/cli-docs-skills/cli-docs/SKILL.md new file mode 100644 index 0000000..f02efea --- /dev/null +++ b/cli-docs-skills/cli-docs/SKILL.md @@ -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`)。如果不确定,先询问用户。 + +--- + +## 第二步:获取版本信息 + +按顺序尝试以下命令,取第一个成功的结果: + +``` + --version + -v + version + -version +``` + +从输出中提取版本号和版本名称(如有,例如 `Docker version 24.0.5, build ced0996`)。如果所有命令都失败,记录"版本信息获取失败"。 + +--- + +## 第三步:获取顶级命令列表 + +按顺序尝试以下命令: + +``` + --help + -h + help + -help +``` + +从输出中解析出所有顶级命令(subcommands)。注意: +- 有些工具把命令分组展示(如 `Management Commands` / `Commands`),需要合并所有组 +- 有些工具的命令用缩进或列表格式呈现,注意识别命令名和其简短描述 +- 全局选项(flags,如 `--verbose`、`-h`)不算命令,不要列入 + +--- + +## 第四步:获取每个子命令的详细帮助 + +对每个顶级命令,运行: + +``` + --help +``` + +如果失败,尝试: + +``` + help + -h +``` + +收集每个命令的: +- 功能说明(一到两句话) +- 用法(usage) +- 重要参数/选项(不需要列出全部,选最常用的) +- 如果该命令本身还有子命令,也递归列出(最多两层深度) + +**子命令数量控制**:如果工具命令非常多(超过 30 个),先列出最核心的命令,并在文档末尾注明"仅列出核心命令,完整列表请运行 ` --help`"。 + +--- + +## 第五步:生成 Markdown 文档 + +### 文档结构模板 + +```markdown +# <工具名> 命令行指南 + +> **版本**:<版本号>(<版本名称,如有>) +> **生成时间**:<当前日期> + +--- + +## 目录 + +- [简介](#简介) +- [命令总览](#命令总览) +- [命令详解](#命令详解) + - [command1](#command1) + - [command2](#command2) + ... + +--- + +## 简介 + +<用 2-3 句话中文介绍该工具是什么、主要用途是什么。从 --help 输出的描述中提炼,结合你对该工具的了解。> + +--- + +## 命令总览 + +共 **N** 个顶级命令,**M** 个子命令(含二级命令)。 + +| 命令 | 简介 | +|------|------| +| `command1` | 一句话中文描述 | +| `command2` | 一句话中文描述 | +... + +--- + +## 命令详解 + +### command1 + +**功能**:<中文说明,2-4 句,解释这个命令做什么、适合什么场景> + +**用法**: +``` + command1 [选项] [参数] +``` + +**常用选项**: + +| 选项 | 说明 | +|------|------| +| `--flag1` | 说明 | +| `--flag2` | 说明 | + +**示例**: +```bash + command1 --flag example +``` + +--- +(每个命令重复上面的格式) +``` + +### 写作要求 + +- **全部用中文**:命令说明、功能介绍、选项描述都用中文。命令名称、选项名称本身保持英文原样。 +- **忠实原文**:中文描述要忠实于 `--help` 输出的含义,不要过度发挥或编造。 +- **通俗易懂**:把技术文档翻译成普通用户能理解的语言,必要时加一句使用场景。 +- **格式整洁**:表格对齐,代码块用正确语言标记(shell/bash)。 + +--- + +## 第六步:输出文件 + +将生成的 Markdown 内容写入文件 `-guide.md`,保存在**用户当前工作目录**(即 Claude Code 运行的目录)。 + +完成后告知用户: +- 文件保存路径(完整路径) +- 共整理了多少个顶级命令、多少个子命令 +- 如有获取失败的命令,列出说明 diff --git a/cli-docs-skills/cli-docs/evals/evals.json b/cli-docs-skills/cli-docs/evals/evals.json new file mode 100644 index 0000000..53dd11d --- /dev/null +++ b/cli-docs-skills/cli-docs/evals/evals.json @@ -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 字符"} + ] + } + ] +} diff --git a/command-help-zh-skill/SKILL.md b/command-help-zh-skill/SKILL.md new file mode 100644 index 0000000..c75d8b0 --- /dev/null +++ b/command-help-zh-skill/SKILL.md @@ -0,0 +1,67 @@ +--- +name: command-help-zh-skill +description: | + 为任意 CLI 命令生成一个独立的 **Bash 可执行脚本**(`-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. 如果没有原始帮助内容,就先运行 ` --help` 获取输出。 +3. 翻译成自然、简洁的中文。 +4. 保留原始结构: + - 标题 + - 简介 + - 用法 + - 命令 + - 参数 + - 选项 +5. 输出时只让说明文字使用 ANSI 黄色。 + - 例如 `\033[1;33m` + - 结束后恢复默认颜色 +6. 命令名、参数、选项、括号内容保持普通颜色,不要整行染色。 +7. 生成可直接放到全局 PATH 的脚本,默认命名为 `-help`;如果用户要求或命名冲突,再用 `-help-zh`。 + +## 实现要求 + +- 必须是独立脚本,不能依赖仓库内外的其他文件。 +- 脚本开头应固定目标命令名,执行时再调用原始命令的 `--help`。 +- 推荐使用 `printf` 逐行输出,避免把整段文本一起染色。 +- 如果需要分隔“左侧标签”和“右侧说明”,先打印标签,再单独打印黄色说明。 +- 保持输出整洁,避免多余前导空格。 + +## 输出标准 + +- 中文表达自然,优先清晰准确。 +- 说明部分高亮黄色。 +- 可以直接复制到 `/usr/local/bin/` 之类的位置使用。 +- 如果用户指定了固定命令名,按用户要求命名。 diff --git a/command-help-zh-skill/agents/openai.yaml b/command-help-zh-skill/agents/openai.yaml new file mode 100644 index 0000000..6194d03 --- /dev/null +++ b/command-help-zh-skill/agents/openai.yaml @@ -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." diff --git a/gitea-tea-skill/.gitattributes b/gitea-tea-skill/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/gitea-tea-skill/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/gitea-tea-skill/.gitignore b/gitea-tea-skill/.gitignore new file mode 100644 index 0000000..65776d1 --- /dev/null +++ b/gitea-tea-skill/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ diff --git a/gitea-tea-skill/SKILL.md b/gitea-tea-skill/SKILL.md new file mode 100644 index 0000000..9129080 --- /dev/null +++ b/gitea-tea-skill/SKILL.md @@ -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. **已知问题与解决方案** + - 旧版 tea(0.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 # 列出所有 Issue(JSON 格式) +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 API(curl) + +当 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/) diff --git a/gitea-tea-skill/reference.md b/gitea-tea-skill/reference.md new file mode 100644 index 0000000..2f5d7eb --- /dev/null +++ b/gitea-tea-skill/reference.md @@ -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 ` | Edit a login entry | +| `tea login delete ` | 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) +``` diff --git a/gitea-tea-skill/scripts/gitea_api.py b/gitea-tea-skill/scripts/gitea_api.py new file mode 100644 index 0000000..822b488 --- /dev/null +++ b/gitea-tea-skill/scripts/gitea_api.py @@ -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 [--desc DESC] [--private] + python scripts/gitea_api.py repos delete + python scripts/gitea_api.py repos exists + 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() diff --git a/gitea-tea-skill/scripts/tea_helper.py b/gitea-tea-skill/scripts/tea_helper.py new file mode 100644 index 0000000..593e59b --- /dev/null +++ b/gitea-tea-skill/scripts/tea_helper.py @@ -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 [--repo owner/repo] + python tea_helper.py close-pr [--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() diff --git a/github-gh-skill/.gitattributes b/github-gh-skill/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/github-gh-skill/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/github-gh-skill/.gitignore b/github-gh-skill/.gitignore new file mode 100644 index 0000000..65776d1 --- /dev/null +++ b/github-gh-skill/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ diff --git a/github-gh-skill/SKILL.md b/github-gh-skill/SKILL.md new file mode 100644 index 0000000..a8040c1 --- /dev/null +++ b/github-gh-skill/SKILL.md @@ -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 CLI(gh)技能 + +## 何时启用 + +用户提到:`gh`、GitHub CLI、命令行操作 GitHub、PR/Issue/Release/Actions、或需要从终端拉取 GitHub 信息时,优先按本技能执行。 + +## 前置检查(一次性) + +1. **检查 `gh` 是否已安装**:执行 `gh --version`。 + - 若命令不存在,**自动安装最新版**:根据系统包管理器执行安装(如 `apt install gh -y`、`yum install gh -y`、`brew install gh`)。 + - 安装失败再**提示用户手动安装**(参考官网 )。 +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 --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 ` +- 查看差异:`gh pr diff ` + +## E. GitHub Actions(workflow / run) + +- 运行列表:`gh run list --limit 20` +- 单次运行详情 + 日志:`gh run view --log` + +## F. Release 与标签 + +- 列表:`gh release list --limit 20` +- 创建(需明确 tag 与说明):`gh release create --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) diff --git a/github-gh-skill/reference.md b/github-gh-skill/reference.md new file mode 100644 index 0000000..b122be6 --- /dev/null +++ b/github-gh-skill/reference.md @@ -0,0 +1,95 @@ +# gh 参考手册(渐进披露) + +主说明见 [SKILL.md](SKILL.md)。此处补充常用命令与非交互参数。 + +## 全局 + +| 用途 | 示例 | +|------|------| +| 帮助 | `gh --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 +gh run watch +gh run rerun +``` + +## 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 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` 版本文档为准。 diff --git a/github-gh-skill/scripts/check_gh.py b/github-gh-skill/scripts/check_gh.py new file mode 100644 index 0000000..258024f --- /dev/null +++ b/github-gh-skill/scripts/check_gh.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +模块:GitHub CLI(gh)跨平台环境检测脚本。 + +说明:跨平台单文件,仅依赖 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() diff --git a/linux-ssh-operator-skill/SKILL.md b/linux-ssh-operator-skill/SKILL.md new file mode 100644 index 0000000..56a239f --- /dev/null +++ b/linux-ssh-operator-skill/SKILL.md @@ -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. diff --git a/linux-ssh-operator-skill/SKILL.zh-CN.md b/linux-ssh-operator-skill/SKILL.zh-CN.md new file mode 100644 index 0000000..8c22616 --- /dev/null +++ b/linux-ssh-operator-skill/SKILL.zh-CN.md @@ -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 安全建议、常见命令、主机指纹处理与故障排查说明。 diff --git a/linux-ssh-operator-skill/agents/openai.yaml b/linux-ssh-operator-skill/agents/openai.yaml new file mode 100644 index 0000000..9792d30 --- /dev/null +++ b/linux-ssh-operator-skill/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Linux SSH Operator" + short_description: "Operate Linux servers, SSH aliases, and passwordless login" diff --git a/linux-ssh-operator-skill/references/ssh-playbook.md b/linux-ssh-operator-skill/references/ssh-playbook.md new file mode 100644 index 0000000..702e0d3 --- /dev/null +++ b/linux-ssh-operator-skill/references/ssh-playbook.md @@ -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). + diff --git a/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh b/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh new file mode 100644 index 0000000..90519b2 --- /dev/null +++ b/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh @@ -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 </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 diff --git a/linux-ssh-operator-skill/scripts/ssh_copy.sh b/linux-ssh-operator-skill/scripts/ssh_copy.sh new file mode 100644 index 0000000..a646c64 --- /dev/null +++ b/linux-ssh-operator-skill/scripts/ssh_copy.sh @@ -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[@]}" diff --git a/linux-ssh-operator-skill/scripts/ssh_run.sh b/linux-ssh-operator-skill/scripts/ssh_run.sh new file mode 100644 index 0000000..2ec38da --- /dev/null +++ b/linux-ssh-operator-skill/scripts/ssh_run.sh @@ -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[@]}" diff --git a/mengya-mail-skill/SKILL.md b/mengya-mail-skill/SKILL.md new file mode 100644 index 0000000..1d6c710 --- /dev/null +++ b/mengya-mail-skill/SKILL.md @@ -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` diff --git a/mengya-mail-skill/scripts/mengya-mail-api.py b/mengya-mail-skill/scripts/mengya-mail-api.py new file mode 100644 index 0000000..7bace76 --- /dev/null +++ b/mengya-mail-skill/scripts/mengya-mail-api.py @@ -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()) diff --git a/mengya-rag-skill/SKILL.md b/mengya-rag-skill/SKILL.md new file mode 100644 index 0000000..a5c4b85 --- /dev/null +++ b/mengya-rag-skill/SKILL.md @@ -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 # 返回条数(默认 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/` diff --git a/mengya-rag-skill/references/output-schema.md b/mengya-rag-skill/references/output-schema.md new file mode 100644 index 0000000..ae25620 --- /dev/null +++ b/mengya-rag-skill/references/output-schema.md @@ -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": "错误描述信息" +} +``` diff --git a/quark-sign-skill/SKILL.md b/quark-sign-skill/SKILL.md new file mode 100644 index 0000000..5826eeb --- /dev/null +++ b/quark-sign-skill/SKILL.md @@ -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 +``` diff --git a/quark-sign-skill/config.json b/quark-sign-skill/config.json new file mode 100644 index 0000000..a48a76c --- /dev/null +++ b/quark-sign-skill/config.json @@ -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" +} diff --git a/quark-sign-skill/config.json.example b/quark-sign-skill/config.json.example new file mode 100644 index 0000000..6b86ceb --- /dev/null +++ b/quark-sign-skill/config.json.example @@ -0,0 +1,3 @@ +{ + "url": "https://drive-m.quark.cn/1/clouddrive/capacity/growth/reward_record?kps=...&sign=...&vcode=..." +} diff --git a/quark-sign-skill/scripts/quark_sign.py b/quark-sign-skill/scripts/quark_sign.py new file mode 100644 index 0000000..a9600bd --- /dev/null +++ b/quark-sign-skill/scripts/quark_sign.py @@ -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()