commit 268ddf7b4379639dadf04f93db260e1a91f5bde2 Author: chendelian <116870791@qq.com> Date: Wed Apr 1 08:44:05 2026 +0800 Add generic Claw skill-template (manifest, CLI, docs, optional snippets) Made-with: Cursor diff --git a/.github/workflows/release_skill.yaml b/.github/workflows/release_skill.yaml new file mode 100644 index 0000000..90076f1 --- /dev/null +++ b/.github/workflows/release_skill.yaml @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# 技能发布工作流(占位) +# ----------------------------------------------------------------------------- +# 不同组织使用不同的复用工作流、制品格式与加密策略。 +# 使用本模板时,请将下面 jobs 整段替换为你们自己的 workflow, +# 或删除本文件若暂不需要 GitHub Actions。 +# +# 设计原则(行业常见): +# - 通过 tag 触发发布(如 v*) +# - 制品与版本号可追溯,与 SKILL.md 中 version 对齐 +# ----------------------------------------------------------------------------- + +name: skill-release-placeholder + +on: + push: + tags: ["v*"] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Placeholder + run: | + echo "Replace this workflow with your organization's reusable workflow." + echo "Example pattern: jobs.release.uses: //.github/workflows/reusable-release-skill.yaml@ref" + exit 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db5cc0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*.pyo +.Python +.venv/ +venv/ +.env +.env.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d79a32 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Claw 技能项目模板(通用) + +本目录是一个**与具体业务无关**的技能(Skill)工程骨架,供团队在各类 **Claw / Agent 宿主**(桌面端、网关、IDE 插件等)上交付可安装技能包时参考。 + +## 你从这里能得到什么 + +- **行业标准对齐**:技能以「清单文件 + 可执行入口 + 文档」组织——与常见的 Agent Skill、CLI 工具包、内部自动化脚本仓库的惯例一致;不绑定某一厂商私有协议。 +- **可移植约定**:数据目录、用户隔离、兄弟技能路径等通过**环境变量契约**描述(见 `docs/RUNTIME.md`);不同宿主只需注入同名变量或做一层别名映射。 +- **低学习成本**:每个文件顶部与关键步骤都有注释;按下面顺序做即可跑通第一个命令。 + +## 建议的上手顺序(约 15~30 分钟) + +1. **复制本模板**为新仓库或新目录,全局把占位符 `your-skill-slug` / `Your Skill Display Name` 换成你的技能标识(与 `SKILL.md` 里 `metadata.skill.slug` 一致)。 +2. **阅读** `docs/RUNTIME.md`,确认你的宿主会注入哪些环境变量;若宿主使用另一套名字,在宿主侧做映射,或改 `optional/paths_snippet.py` 中的读取顺序(文件内有说明)。 +3. **本地试跑**:`python scripts/skill_main.py health` 应输出成功信息。 +4. **扩展子命令**:在 `scripts/skill_main.py` 的 `dispatch` 中增加分支;业务逻辑放在同目录其它模块或子包中,保持入口轻薄。 +5. **编写/调整 `SKILL.md`**:只改「何时触发、如何调用、参数含义」,不要写实现细节;实现细节放在 `docs/` 或代码注释里。 +6. **发布**:若使用 GitHub Actions,编辑 `.github/workflows/release_skill.yaml`,把 `uses:` 指向**你们组织**的复用工作流;若不用 CI,可删除该目录。 + +## 目录一览 + +| 路径 | 作用 | +|------|------| +| `SKILL.md` | 技能清单(YAML 头 + Markdown 正文),供宿主与协作者阅读 | +| `scripts/skill_main.py` | 推荐唯一 CLI 入口;含 `health` / `version` 示例 | +| `docs/RUNTIME.md` | 环境与目录契约(多宿主通用) | +| `docs/SKILL_TYPES.md` | 常见技能形态与自检清单 | +| `docs/PORTABILITY.md` | 多 Claw 宿主差异与兼容建议 | +| `optional/` | 可选复制进项目的片段(路径、SQLite 示例),**不默认 import** | + +## 不要做的事 + +- 不要在模板中提交真实密钥、真实业务表结构或平台专用逻辑。 +- 不要把模板改成只支持某一种宿主;特殊项写在 `docs/PORTABILITY.md` 的「宿主附录」中。 + +## 版本 + +模板自身版本见 `SKILL.md` 的 `version` 字段;与你技能的业务版本一致更新即可。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..d144e7b --- /dev/null +++ b/SKILL.md @@ -0,0 +1,56 @@ +--- +# --------------------------------------------------------------------------- +# 技能清单(Skill Manifest) +# 行业常见做法:YAML 头描述元数据,正文 Markdown 描述「何时用、怎么调」。 +# 不同 Claw 宿主解析字段可能略有差异;可移植标识请使用 metadata.skill.slug。 +# --------------------------------------------------------------------------- +name: 您的技能显示名称 +description: 一句话说明技能做什么(给编排层与人类阅读,不写实现细节)。 +version: 1.0.0 +author: 深圳匠厂科技有限公司 +metadata: + skill: + # 机器可读、稳定标识:小写字母、数字、短横线;与数据子目录名一致。 + slug: your-skill-slug + emoji: "📦" + category: "通用" +# 宿主若限制可调用的工具类型,在此列出(按宿主文档填写)。 +allowed-tools: + - bash +--- + +# Your Skill Display Name + +## 使用时机 + + + +- 示例:用户说「检查技能是否可用」「运行某某任务」 + +## 执行步骤 + + + +### 健康检查(推荐自动化先跑) + +```bash +python3 {baseDir}/scripts/skill_main.py health +``` + +### 查看版本 + +```bash +python3 {baseDir}/scripts/skill_main.py version +``` + +### 扩展子命令 + +在 `scripts/skill_main.py` 中增加新的子命令分支,并在此处追加对应示例命令与参数说明。 + +## 环境依赖 + +详见本仓库 `docs/RUNTIME.md`(`CLAW_DATA_ROOT`、`CLAW_USER_ID` 等)。 + +## 数据与隐私 + +本技能若产生持久化数据,应仅写入 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/your-skill-slug/` 下;不得将用户数据提交到版本库。 diff --git a/docs/PORTABILITY.md b/docs/PORTABILITY.md new file mode 100644 index 0000000..54a21a6 --- /dev/null +++ b/docs/PORTABILITY.md @@ -0,0 +1,56 @@ +# 多宿主(多 Claw)可移植性说明 + +## 设计目标 + +同一套技能仓库应能在**不同 Claw 实现**下工作,只要宿主满足本文档的**最小契约**:能启动进程、传入环境变量、并把 `SKILL.md` 提供给编排层阅读。 + +## 行业上常见的「技能包」形态 + +- **声明式清单**(Markdown + YAML 头):描述名称、描述、版本、工具权限、触发场景。 +- **可执行入口**:一至多个脚本/二进制,由宿主通过 `bash` / `python` 等调用。 +- **用户数据与代码分离**:持久化数据落在用户可写目录,不写在安装目录内。 + +本模板按上述惯例组织;具体宿主如何解析 `SKILL.md` 的 YAML 键名,以各宿主文档为准。 + +## 标识技能:`metadata.skill` + +本模板在 `SKILL.md` 中使用 **`metadata.skill.slug`** 作为**可移植**的机器可读标识(短横线命名,如 `my-skill`)。 + +若你的宿主仍要求其它键名(例如历史实现里的嵌套字段),请在宿主侧做**映射**,或在 `SKILL.md` 中**并列声明**两组 metadata(保持 `slug` 值一致)。不要在业务代码里写死某一宿主品牌名。 + +## 环境变量:推荐前缀 `CLAW_*` + +为减少对单一产品名的耦合,模板文档与可选片段推荐使用: + +| 变量 | 含义 | +|------|------| +| `CLAW_DATA_ROOT` | 用户数据根目录(多技能共享的上一级) | +| `CLAW_USER_ID` | 当前工作空间或用户标识,用于数据隔离 | +| `CLAW_SKILLS_ROOT` | 可选;多个技能并排安装时的根目录,便于 `subprocess` 调用兄弟技能 | + +宿主若已使用其它名称,推荐在**启动子进程时**注入别名,例如: + +- 将宿主内部的「数据根」映射为 `CLAW_DATA_ROOT` +- 将宿主内部「用户 ID」映射为 `CLAW_USER_ID` + +这样技能脚本无需分支判断宿主品牌。 + +## 路径布局约定(逻辑路径) + +在 `CLAW_DATA_ROOT` 与 `CLAW_USER_ID` 可用时,本技能推荐将私有数据放在: + +```text +{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/ +``` + +其中 `skill_slug` 与 `SKILL.md` 中 `metadata.skill.slug` 一致。若环境变量缺失,`optional/paths_snippet.py` 中提供了**仅用于开发机**的 fallback(见该文件注释),生产环境应由宿主注入变量。 + +## 发布与制品 + +不同组织对加密、签名、制品格式要求不同。`.github/workflows/release_skill.yaml` 仅作占位:**务必替换**为你们自己的复用工作流或删除。模板不假设必须使用某一家的发布流水线。 + +## 自检 + +- [ ] `SKILL.md` 中 `slug` 与目录名/制品名策略是否与宿主一致 +- [ ] 宿主文档要求的环境变量是否已全部注入 +- [ ] 是否在文档中说明了「未注入变量时的行为」(拒绝运行 / 本地 fallback) diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md new file mode 100644 index 0000000..004e8ce --- /dev/null +++ b/docs/RUNTIME.md @@ -0,0 +1,55 @@ +# 运行时契约(环境变量与目录) + +本文档定义技能进程**建议依赖**的外部条件,便于不同 Claw 宿主统一接入。业务技能应**读取环境变量**,而不是在代码里写死路径或用户名。 + +## 必需程度说明 + +- **强烈建议**:生产环境由宿主注入;技能应对缺失给出明确错误提示,避免静默写到意外目录。 +- **可选**:没有时技能仍可部分运行(例如只读 `health`)。 + +## 变量一览 + +### `CLAW_DATA_ROOT`(强烈建议) + +用户数据根。多个技能、多个用户的数据都在此根之下分区。 + +- 典型场景:组织策略指定的盘符路径或 `~/.your-org-data`。 +- 未设置时:技能**不应**猜测网络盘;开发机 fallback 仅限 `optional/paths_snippet.py` 中说明的情形。 + +### `CLAW_USER_ID`(强烈建议) + +当前会话所代表的用户或工作空间 ID(字符串)。与数据隔离强相关。 + +- 用于拼接:`{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/` +- 未设置时:可用匿名占位(如 `_anon`)**仅用于开发**,生产应显式注入。 + +### `CLAW_SKILLS_ROOT`(可选) + +多个技能以并列目录安装时的根路径,例如: + +```text +{CLAW_SKILLS_ROOT}/skill-a/scripts/... +{CLAW_SKILLS_ROOT}/skill-b/scripts/... +``` + +编排型技能若需要通过子进程调用兄弟技能,应基于该变量定位脚本,避免写死绝对路径。 + +## 本技能推荐的数据目录 + +```text +{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/ +``` + +- `skill_slug`:与 `SKILL.md` 内 `metadata.skill.slug` 一致。 +- 在此目录下可放置 SQLite 文件、缓存、上传临时文件等;**不要**向版本库提交该目录内容。 + +## 标准输出约定(建议) + +为便于宿主与自动化解析,建议: + +- 致命错误:单行前缀 `ERROR:`,例如 `ERROR:MISSING_ENV_CLAW_DATA_ROOT` +- 成功:人类可读一行或多行;若有机读需求,可用 `JSON` 单行输出并在 `SKILL.md` 中说明格式。 + +## 与具体宿主的关系 + +若某宿主文档规定了另一套变量名,应在**宿主启动技能子进程时**注入为本文档中的 `CLAW_*` 名称,或在技能内使用一层极薄的 `getenv` 封装(见 `optional/paths_snippet.py` 注释示例)。**不要在业务模块中散落多套变量名判断。** diff --git a/docs/SKILL_TYPES.md b/docs/SKILL_TYPES.md new file mode 100644 index 0000000..80c8590 --- /dev/null +++ b/docs/SKILL_TYPES.md @@ -0,0 +1,33 @@ +# 技能形态与自检清单(无业务) + +开发前先选定形态,避免把「编排、存储、浏览器」混在一个脚本里难以测试。 + +## 类型 A:无状态工具型 + +- **特征**:不持久化用户数据,或只读配置文件;输入输出主要在 stdin/stdout。 +- **数据目录**:通常不需要 `CLAW_DATA_ROOT` 下的专属库;若需要缓存,仍建议放在契约目录下。 +- **自检**:离线可跑;`health` 不访问网络也可成功。 + +## 类型 B:本地持久化型 + +- **特征**:使用 SQLite、本地文件等保存用户数据。 +- **数据目录**:必须使用 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/`。 +- **自检**:首次运行自动建库/建表;文档中说明库文件路径与备份方式。 + +## 类型 C:编排型(调用其它技能或外部 CLI) + +- **特征**:自身逻辑薄,主要 `subprocess` 或 HTTP 调用其它组件。 +- **依赖**:在 `SKILL.md` 中明确写出**先决条件**(兄弟技能已安装、某 CLI 在 PATH 等)。 +- **自检**:`health` 可检查兄弟可执行文件是否存在;缺失时打印清晰错误。 + +## 类型 D:混合型 + +- **特征**:既有本地存储,又调用外部能力。 +- **建议**:拆模块(存储 / 编排 / 领域逻辑),入口脚本只做参数解析与调度。 + +## 发布前通用自检 + +- [ ] `SKILL.md` 中触发条件与示例命令与实际入口一致 +- [ ] 未注入 `CLAW_DATA_ROOT` / `CLAW_USER_ID` 时行为已文档化 +- [ ] 不向仓库提交用户数据、密钥、大型二进制 +- [ ] 错误信息包含「如何修复」(缺什么环境变量、缺哪个依赖) diff --git a/optional/README.md b/optional/README.md new file mode 100644 index 0000000..9432856 --- /dev/null +++ b/optional/README.md @@ -0,0 +1,17 @@ +# optional/ 目录说明 + +本目录下的文件**不会**被 `scripts/skill_main.py` 自动引用。 + +## 为什么要单独放 + +- 避免模板入口脚本依赖过多,同事从「最小 health」起步时零干扰。 +- 需要时**整文件复制**到 `scripts/` 或你们自己的包路径下,再按文件头注释改名、改常量。 + +## 文件列表 + +| 文件 | 用途 | +|------|------| +| `paths_snippet.py` | `CLAW_*` 数据目录解析与 fallback 说明 | +| `sqlite_minimal.py` | 无业务含义的 SQLite 建表示例 | + +复制后务必全局替换 `your-skill-slug` 与表名,避免与别的技能冲突。 diff --git a/optional/paths_snippet.py b/optional/paths_snippet.py new file mode 100644 index 0000000..21d3d96 --- /dev/null +++ b/optional/paths_snippet.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +可选片段:路径与环境变量(请复制到 scripts/ 或作为独立模块使用) +================================================================ + +【用途】 + 统一解析「用户数据根」「用户 ID」「本技能私有目录」,避免在业务代码里重复 os.getenv。 + +【使用步骤】 + 1. 将本文件复制到 skills/your-slug/scripts/paths.py(或任意模块名)。 + 2. 把 SKILL_SLUG 改为与 SKILL.md 中 metadata.skill.slug 一致。 + 3. 在业务代码中: from paths import get_skill_data_dir (按实际包路径调整) + +【可移植性】 + - 优先读取标准名 CLAW_*(见 docs/RUNTIME.md)。 + - 若你的组织在宿主侧仍使用历史变量名,可在此文件 _aliases 列表中追加 (标准名, 备选名), + 由宿主注入其一即可;不要在业务里再写第三套名字。 + +【注意】 + - 未设置 CLAW_DATA_ROOT 时的 fallback 仅适合开发机;生产环境应由宿主注入。 +""" + +from __future__ import annotations + +import os +import sys + +# TODO: 复制本文件后改为你的 slug(与 SKILL.md metadata.skill.slug 一致) +SKILL_SLUG = "your-skill-slug" + + +def _getenv_first(names: tuple[str, ...]) -> str: + """按顺序读取多个环境变量名,返回第一个非空值。""" + for n in names: + v = (os.getenv(n) or "").strip() + if v: + return v + return "" + + +def get_data_root() -> str: + """ + 用户数据根目录。 + 顺序:CLAW_DATA_ROOT → (可选)宿主别名,见下方元组。 + 若皆空:Windows 默认 D:\\claw-data;其它系统默认 ~/.claw-data —— 仅开发便利,生产请注入 CLAW_DATA_ROOT。 + """ + root = _getenv_first( + ( + "CLAW_DATA_ROOT", + # 在此追加组织内别名,例如 "MYORG_USER_DATA_ROOT", + ) + ) + if root: + return root + if sys.platform == "win32": + return r"D:\claw-data" + return os.path.join(os.path.expanduser("~"), ".claw-data") + + +def get_user_id() -> str: + """当前用户或工作空间 ID;未设置时用 _anon(仅开发)。""" + uid = _getenv_first( + ( + "CLAW_USER_ID", + # 在此追加别名,例如 "MYORG_WORKSPACE_ID", + ) + ) + return uid or "_anon" + + +def get_skill_data_dir() -> str: + """ + 本技能可写目录:{数据根}/{用户ID}/{skill_slug}/ + 会自动 os.makedirs(..., exist_ok=True)。 + """ + path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG) + os.makedirs(path, exist_ok=True) + return path + + +def get_skills_root() -> str: + """ + 可选:并列安装的多技能根目录。 + 未设置 CLAW_SKILLS_ROOT 时,默认使用本文件所在仓库的上一级目录的上一级 + (即:.../skill-template/scripts/ → 误推断;复制后请改为 BASE_DIR 逻辑)。 + """ + root = _getenv_first(("CLAW_SKILLS_ROOT",)) + if root: + return root + # 模板占位:脚本位于 scripts/,仓库根为 dirname(dirname(__file__)) + here = os.path.dirname(os.path.abspath(__file__)) + return os.path.dirname(here) diff --git a/optional/sqlite_minimal.py b/optional/sqlite_minimal.py new file mode 100644 index 0000000..89c0b9b --- /dev/null +++ b/optional/sqlite_minimal.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +可选片段:最小 SQLite 示例(请复制后按需修改) +============================================== + +【用途】 + 演示「单表 + 自增主键 + INTEGER 时间戳」的一种严谨写法;与具体业务无关。 + +【使用步骤】 + 1. 复制到 scripts/db_example.py(或并入你的模块)。 + 2. 修改 TABLE_SQL 中的表名与字段;保持时间字段为 INTEGER Unix 秒(UTC)若需跨时区一致。 + 3. 在入口脚本中仅在需要持久化时 import。 + +【注意】 + - 不在模板中做迁移兼容;schema 变更请用你们组织的迁移策略。 + - 数据库文件路径建议:get_skill_data_dir() / "skill.db"(paths_snippet 中函数)。 +""" + +from __future__ import annotations + +import sqlite3 +import time + +# 示例表:与任何业务无关 +TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS template_audit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键 + action TEXT NOT NULL, -- 动作标识,如 health_ok + created_at INTEGER NOT NULL -- Unix 秒 UTC +); +""" + + +def connect(db_path: str) -> sqlite3.Connection: + return sqlite3.connect(db_path) + + +def init_db(conn: sqlite3.Connection) -> None: + conn.execute(TABLE_SQL) + conn.commit() + + +def record_action(conn: sqlite3.Connection, action: str) -> None: + conn.execute( + "INSERT INTO template_audit (action, created_at) VALUES (?, ?)", + (action, int(time.time())), + ) + conn.commit() diff --git a/scripts/skill_main.py b/scripts/skill_main.py new file mode 100644 index 0000000..93a70b4 --- /dev/null +++ b/scripts/skill_main.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +技能入口脚本(模板) +==================== + +【职责】 + - 作为宿主调用时的统一 CLI 入口(子进程、终端、CI 均可)。 + - 只做:参数解析、环境检查、分发到具体子命令;复杂逻辑放到同目录其它模块。 + +【如何扩展】 + 1. 在 main() 的 dispatch 字典中增加 "your_cmd": handler 项。 + 2. 实现 handler(argv) 或 handler();出错时打印 ERROR: 前缀信息并 sys.exit(非0)。 + 3. 在仓库根目录 SKILL.md「执行步骤」中补充示例命令。 + +【多宿主注意】 + - 不要在本文件写死某一品牌宿主名。 + - 路径与环境变量约定见 ../docs/RUNTIME.md;可选辅助代码见 ../optional/paths_snippet.py(需自行复制或 import 路径按项目调整)。 + +【编码】 + Windows 下若宿主仍使用系统默认编码,可在宿主侧设置 UTF-8;本模板不强制改 sys.stdout(避免与宿主捕获冲突)。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from typing import Callable, Dict, List, Optional + +# 与 SKILL.md 中 metadata.skill.slug 保持一致(模板占位,复制后请修改) +SKILL_SLUG = "your-skill-slug" + + +def cmd_version(_args: argparse.Namespace) -> int: + """打印版本信息(与 SKILL.md frontmatter 中 version 应对齐,此处为占位)。""" + payload = { + "skill_slug": SKILL_SLUG, + "version": "0.1.0", + "entry": "skill_main.py", + } + print(json.dumps(payload, ensure_ascii=False)) + return 0 + + +def cmd_health(_args: argparse.Namespace) -> int: + """ + 健康检查:应快速、可离线(除非技能本身强依赖网络)。 + 失败时打印 ERROR: 前缀,便于宿主与自动化解析。 + """ + # 示例:检查 Python 版本(可按需改为检查关键依赖 import) + if sys.version_info < (3, 9): + print("ERROR:PYTHON_VERSION need >= 3.9", file=sys.stderr) + return 1 + print(f"OK skill={SKILL_SLUG} python={sys.version.split()[0]}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description="Claw skill CLI template — replace SKILL_SLUG and add subcommands.", + ) + sub = p.add_subparsers(dest="command", required=True) + + sp = sub.add_parser("version", help="Print version JSON.") + sp.set_defaults(handler=cmd_version) + + sp = sub.add_parser("health", help="Quick health check.") + sp.set_defaults(handler=cmd_health) + + return p + + +def main(argv: Optional[List[str]] = None) -> int: + argv = argv if argv is not None else sys.argv[1:] + parser = build_parser() + args = parser.parse_args(argv) + handler: Callable[[argparse.Namespace], int] = args.handler + return handler(args) + + +if __name__ == "__main__": + raise SystemExit(main())