@clawhub-52yuanchangxing-8112df52fd
对一批 Skill 选题按高频、低门槛、传播性、ROI 和风险做排序。;use for skills, market-fit, prioritization workflows;do not use for 只按个人偏好排序, 忽略发布风险.
---
name: skill-market-fit-ranker
version: 1.0.0
description: "对一批 Skill 选题按高频、低门槛、传播性、ROI 和风险做排序。;use for skills, market-fit, prioritization workflows;do not use for 只按个人偏好排序, 忽略发布风险."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/skill-market-fit-ranker
tags: [skills, market-fit, prioritization, portfolio]
user-invocable: true
metadata: {"openclaw":{"emoji":"📊","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# Skill 市场匹配排序器
## 你是什么
你是“Skill 市场匹配排序器”这个独立 Skill,负责:对一批 Skill 选题按高频、低门槛、传播性、ROI 和风险做排序。
## Routing
### 适合使用的情况
- 帮我给这批 skill 排序
- 综合高频和传播性
- 输入通常包含:skill 列表、目标用户、发布目标
- 优先产出:评分维度、排序结果、建议路线图
### 不适合使用的情况
- 不要只按个人偏好排序
- 不要忽略发布风险
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 评分维度
- 排序结果
- 高潜题目
- 低优先级题目
- 风险说明
- 建议路线图
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 输出为组合决策辅助。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# Skill 市场匹配排序器
## 功能
对一批 Skill 选题按高频、低门槛、传播性、ROI 和风险做排序。
## 适用场景
- 选题排序
- 路线图决策
- 发行规划
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:skill 列表、目标用户、发布目标
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:输出为组合决策辅助。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我给这批 skill 排序
- 综合高频和传播性
## 输入输出示例
### 输入侧重点
- 评分维度
- 排序结果
- 高潜题目
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# Skill 市场匹配排序器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# Skill 市场匹配排序器 示例输入
目标:选题排序
输入类型:skill 列表、目标用户、发布目标
## 背景
- 这是一个用于演示 Skill 市场匹配排序器 的最小可复核样例。
- 希望产出与“评分维度 / 排序结果 / 建议路线图”相关的结构化结果。
## 原始材料
- 主题:Skill 市场匹配排序器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# Skill 市场匹配排序器 示例输出
## 评分维度
- 这里是与“评分维度”相关的示例条目。
## 排序结果
- 这里是与“排序结果”相关的示例条目。
## 高潜题目
- 这里是与“高潜题目”相关的示例条目。
## 低优先级题目
- 这里是与“低优先级题目”相关的示例条目。
## 风险说明
- 这里是与“风险说明”相关的示例条目。
## 建议路线图
- 这里是与“建议路线图”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "skill-market-fit-ranker",
"title": "Skill 市场匹配排序器",
"category": "skills",
"categoryLabel": "Skill 生态与发布工程",
"mode": "structured_brief",
"summary": "对一批 Skill 选题按高频、低门槛、传播性、ROI 和风险做排序。",
"inputHint": "skill 列表、目标用户、发布目标",
"sections": [
"评分维度",
"排序结果",
"高潜题目",
"低优先级题目",
"风险说明",
"建议路线图"
],
"useCases": [
"选题排序",
"路线图决策",
"发行规划"
],
"positiveExamples": [
"帮我给这批 skill 排序",
"综合高频和传播性"
],
"negativeExamples": [
"不要只按个人偏好排序",
"不要忽略发布风险"
],
"risk": "输出为组合决策辅助。",
"tags": [
"skills",
"market-fit",
"prioritization",
"portfolio"
]
}
FILE:resources/template.md
# Skill 市场匹配排序器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 评分维度
- 待填写:围绕“评分维度”给出与 Skill 市场匹配排序器 场景相关的内容。
## 排序结果
- 待填写:围绕“排序结果”给出与 Skill 市场匹配排序器 场景相关的内容。
## 高潜题目
- 待填写:围绕“高潜题目”给出与 Skill 市场匹配排序器 场景相关的内容。
## 低优先级题目
- 待填写:围绕“低优先级题目”给出与 Skill 市场匹配排序器 场景相关的内容。
## 风险说明
- 待填写:围绕“风险说明”给出与 Skill 市场匹配排序器 场景相关的内容。
## 建议路线图
- 待填写:围绕“建议路线图”给出与 Skill 市场匹配排序器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# Skill 市场匹配排序器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 评分维度
- 排序结果
- 建议路线图
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
安装前验证二进制、环境变量、配置、OS 与 sandbox 条件,解释为什么此机不适合装。;use for skills, install, preflight workflows;do not use for 假装依赖已经满足, 直接修改系统环境.
---
name: skill-install-checker
version: 1.0.0
description: "安装前验证二进制、环境变量、配置、OS 与 sandbox 条件,解释为什么此机不适合装。;use for skills, install, preflight workflows;do not use for 假装依赖已经满足, 直接修改系统环境."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/skill-install-checker
tags: [skills, install, preflight, environment]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧰","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# Skill 安装条件检查器
## 你是什么
你是“Skill 安装条件检查器”这个独立 Skill,负责:安装前验证二进制、环境变量、配置、OS 与 sandbox 条件,解释为什么此机不适合装。
## Routing
### 适合使用的情况
- 检查这台机器能不能装这个 skill
- 说明缺哪些依赖
- 输入通常包含:skill 目录、目标机器环境
- 优先产出:环境概览、缺失依赖、回滚建议
### 不适合使用的情况
- 不要假装依赖已经满足
- 不要直接修改系统环境
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 环境概览
- 缺失依赖
- OS 与 sandbox 风险
- 配置缺口
- 安装建议
- 回滚建议
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 只做环境检查与解释。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# Skill 安装条件检查器
## 功能
安装前验证二进制、环境变量、配置、OS 与 sandbox 条件,解释为什么此机不适合装。
## 适用场景
- 安装前检查
- 兼容性说明
- 支持排障
## 推荐实现边界
- 模式:`skill_audit` —— 检查 Skill 目录的 frontmatter、依赖声明和必备文件。
- 输入:skill 目录、目标机器环境
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:只做环境检查与解释。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 检查这台机器能不能装这个 skill
- 说明缺哪些依赖
## 输入输出示例
### 输入侧重点
- 环境概览
- 缺失依赖
- OS 与 sandbox 风险
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# Skill 安装条件检查器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# Skill 安装条件检查器 示例输入
目标:安装前检查
输入类型:skill 目录、目标机器环境
## 背景
- 这是一个用于演示 Skill 安装条件检查器 的最小可复核样例。
- 希望产出与“环境概览 / 缺失依赖 / 回滚建议”相关的结构化结果。
## 原始材料
- 主题:Skill 安装条件检查器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# Skill 安装条件检查器 示例输出
## 环境概览
- 这里是与“环境概览”相关的示例条目。
## 缺失依赖
- 这里是与“缺失依赖”相关的示例条目。
## OS 与 sandbox 风险
- 这里是与“OS 与 sandbox 风险”相关的示例条目。
## 配置缺口
- 这里是与“配置缺口”相关的示例条目。
## 安装建议
- 这里是与“安装建议”相关的示例条目。
## 回滚建议
- 这里是与“回滚建议”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "skill-install-checker",
"title": "Skill 安装条件检查器",
"category": "skills",
"categoryLabel": "Skill 生态与发布工程",
"mode": "skill_audit",
"summary": "安装前验证二进制、环境变量、配置、OS 与 sandbox 条件,解释为什么此机不适合装。",
"inputHint": "skill 目录、目标机器环境",
"sections": [
"环境概览",
"缺失依赖",
"OS 与 sandbox 风险",
"配置缺口",
"安装建议",
"回滚建议"
],
"useCases": [
"安装前检查",
"兼容性说明",
"支持排障"
],
"positiveExamples": [
"检查这台机器能不能装这个 skill",
"说明缺哪些依赖"
],
"negativeExamples": [
"不要假装依赖已经满足",
"不要直接修改系统环境"
],
"risk": "只做环境检查与解释。",
"tags": [
"skills",
"install",
"preflight",
"environment"
]
}
FILE:resources/template.md
# Skill 安装条件检查器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 环境概览
- 待填写:围绕“环境概览”给出与 Skill 安装条件检查器 场景相关的内容。
## 缺失依赖
- 待填写:围绕“缺失依赖”给出与 Skill 安装条件检查器 场景相关的内容。
## OS 与 sandbox 风险
- 待填写:围绕“OS 与 sandbox 风险”给出与 Skill 安装条件检查器 场景相关的内容。
## 配置缺口
- 待填写:围绕“配置缺口”给出与 Skill 安装条件检查器 场景相关的内容。
## 安装建议
- 待填写:围绕“安装建议”给出与 Skill 安装条件检查器 场景相关的内容。
## 回滚建议
- 待填写:围绕“回滚建议”给出与 Skill 安装条件检查器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# Skill 安装条件检查器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 环境概览
- 缺失依赖
- 回滚建议
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
扫描现有 Skill 套装,找重复、缺口、职责冲突和最该补的空位。;use for skills, bundle, analysis workflows;do not use for 臆造目录内容, 直接删除 skill.
---
name: skill-gap-finder
version: 1.0.0
description: "扫描现有 Skill 套装,找重复、缺口、职责冲突和最该补的空位。;use for skills, bundle, analysis workflows;do not use for 臆造目录内容, 直接删除 skill."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/skill-gap-finder
tags: [skills, bundle, analysis, architecture]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧭","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# Skill 缺口发现器
## 你是什么
你是“Skill 缺口发现器”这个独立 Skill,负责:扫描现有 Skill 套装,找重复、缺口、职责冲突和最该补的空位。
## Routing
### 适合使用的情况
- 扫描我的技能包找缺口
- 看哪些 skill 职责重叠
- 输入通常包含:skills 根目录路径
- 优先产出:现有分布、重复职责、发布建议
### 不适合使用的情况
- 不要臆造目录内容
- 不要直接删除 skill
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 现有分布
- 重复职责
- 关键缺口
- 冲突点
- 优先补位
- 发布建议
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 只做套装分析与建议。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# Skill 缺口发现器
## 功能
扫描现有 Skill 套装,找重复、缺口、职责冲突和最该补的空位。
## 适用场景
- 技能规划
- 套装治理
- 发布策略
## 推荐实现边界
- 模式:`directory_audit` —— 只读扫描目录或文件清单,输出结构和风险报告。
- 输入:skills 根目录路径
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:只做套装分析与建议。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 扫描我的技能包找缺口
- 看哪些 skill 职责重叠
## 输入输出示例
### 输入侧重点
- 现有分布
- 重复职责
- 关键缺口
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# Skill 缺口发现器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# Skill 缺口发现器 示例输入
目标:技能规划
输入类型:skills 根目录路径
## 背景
- 这是一个用于演示 Skill 缺口发现器 的最小可复核样例。
- 希望产出与“现有分布 / 重复职责 / 发布建议”相关的结构化结果。
## 原始材料
- 主题:Skill 缺口发现器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# Skill 缺口发现器 示例输出
## 现有分布
- 这里是与“现有分布”相关的示例条目。
## 重复职责
- 这里是与“重复职责”相关的示例条目。
## 关键缺口
- 这里是与“关键缺口”相关的示例条目。
## 冲突点
- 这里是与“冲突点”相关的示例条目。
## 优先补位
- 这里是与“优先补位”相关的示例条目。
## 发布建议
- 这里是与“发布建议”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "skill-gap-finder",
"title": "Skill 缺口发现器",
"category": "skills",
"categoryLabel": "Skill 生态与发布工程",
"mode": "directory_audit",
"summary": "扫描现有 Skill 套装,找重复、缺口、职责冲突和最该补的空位。",
"inputHint": "skills 根目录路径",
"sections": [
"现有分布",
"重复职责",
"关键缺口",
"冲突点",
"优先补位",
"发布建议"
],
"useCases": [
"技能规划",
"套装治理",
"发布策略"
],
"positiveExamples": [
"扫描我的技能包找缺口",
"看哪些 skill 职责重叠"
],
"negativeExamples": [
"不要臆造目录内容",
"不要直接删除 skill"
],
"risk": "只做套装分析与建议。",
"tags": [
"skills",
"bundle",
"analysis",
"architecture"
]
}
FILE:resources/template.md
# Skill 缺口发现器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 现有分布
- 待填写:围绕“现有分布”给出与 Skill 缺口发现器 场景相关的内容。
## 重复职责
- 待填写:围绕“重复职责”给出与 Skill 缺口发现器 场景相关的内容。
## 关键缺口
- 待填写:围绕“关键缺口”给出与 Skill 缺口发现器 场景相关的内容。
## 冲突点
- 待填写:围绕“冲突点”给出与 Skill 缺口发现器 场景相关的内容。
## 优先补位
- 待填写:围绕“优先补位”给出与 Skill 缺口发现器 场景相关的内容。
## 发布建议
- 待填写:围绕“发布建议”给出与 Skill 缺口发现器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# Skill 缺口发现器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 现有分布
- 重复职责
- 发布建议
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
修复 frontmatter、metadata、requires、install 和目录结构问题,贴合当前 OpenClaw 规范。;use for skills, frontmatter, validation workflows;do not use for 编造不存在的字段, 忽略单行 JSON 约束.
---
name: skill-frontmatter-doctor
version: 1.0.0
description: "修复 frontmatter、metadata、requires、install 和目录结构问题,贴合当前 OpenClaw 规范。;use for skills, frontmatter, validation workflows;do not use for 编造不存在的字段, 忽略单行 JSON 约束."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/skill-frontmatter-doctor
tags: [skills, frontmatter, validation, openclaw]
user-invocable: true
metadata: {"openclaw":{"emoji":"🩺","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# Skill Frontmatter 医生
## 你是什么
你是“Skill Frontmatter 医生”这个独立 Skill,负责:修复 frontmatter、metadata、requires、install 和目录结构问题,贴合当前 OpenClaw 规范。
## Routing
### 适合使用的情况
- 检查这个 skill 的 frontmatter 合不合规
- 修 requires 和 metadata
- 输入通常包含:单个 skill 或 skills 根目录
- 优先产出:前言问题、metadata 问题、复检步骤
### 不适合使用的情况
- 不要编造不存在的字段
- 不要忽略单行 JSON 约束
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 前言问题
- metadata 问题
- requires/install 问题
- 目录问题
- 修复建议
- 复检步骤
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 聚焦规范修复与校验。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# Skill Frontmatter 医生
## 功能
修复 frontmatter、metadata、requires、install 和目录结构问题,贴合当前 OpenClaw 规范。
## 适用场景
- 发布修复
- 规范校验
- 批量整理
## 推荐实现边界
- 模式:`skill_audit` —— 检查 Skill 目录的 frontmatter、依赖声明和必备文件。
- 输入:单个 skill 或 skills 根目录
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:聚焦规范修复与校验。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 检查这个 skill 的 frontmatter 合不合规
- 修 requires 和 metadata
## 输入输出示例
### 输入侧重点
- 前言问题
- metadata 问题
- requires/install 问题
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# Skill Frontmatter 医生 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# Skill Frontmatter 医生 示例输入
目标:发布修复
输入类型:单个 skill 或 skills 根目录
## 背景
- 这是一个用于演示 Skill Frontmatter 医生 的最小可复核样例。
- 希望产出与“前言问题 / metadata 问题 / 复检步骤”相关的结构化结果。
## 原始材料
- 主题:Skill Frontmatter 医生 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# Skill Frontmatter 医生 示例输出
## 前言问题
- 这里是与“前言问题”相关的示例条目。
## metadata 问题
- 这里是与“metadata 问题”相关的示例条目。
## requires/install 问题
- 这里是与“requires/install 问题”相关的示例条目。
## 目录问题
- 这里是与“目录问题”相关的示例条目。
## 修复建议
- 这里是与“修复建议”相关的示例条目。
## 复检步骤
- 这里是与“复检步骤”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:out.md
# Skill Frontmatter 医生 规范检查
检查目标:`/mnt/data/clawhub-100-innovative-skills-bundle/skill-frontmatter-doctor`
## 文件完整性
- SKILL.md: OK
- README.md: OK
- SELF_CHECK.md: OK
- scripts/run.py: OK
- resources/spec.json: OK
- resources/template.md: OK
- examples/example-input.md: OK
- tests/smoke-test.md: OK
## Frontmatter
- name: OK
- description: OK
- version: OK
- metadata: OK
- metadata JSON: OK
## 前言问题
- 围绕“前言问题”给出修复建议或复检动作。
## metadata 问题
- 围绕“metadata 问题”给出修复建议或复检动作。
## requires/install 问题
- 围绕“requires/install 问题”给出修复建议或复检动作。
## 目录问题
- 围绕“目录问题”给出修复建议或复检动作。
## 修复建议
- 围绕“修复建议”给出修复建议或复检动作。
## 复检步骤
- 围绕“复检步骤”给出修复建议或复检动作。
FILE:resources/spec.json
{
"slug": "skill-frontmatter-doctor",
"title": "Skill Frontmatter 医生",
"category": "skills",
"categoryLabel": "Skill 生态与发布工程",
"mode": "skill_audit",
"summary": "修复 frontmatter、metadata、requires、install 和目录结构问题,贴合当前 OpenClaw 规范。",
"inputHint": "单个 skill 或 skills 根目录",
"sections": [
"前言问题",
"metadata 问题",
"requires/install 问题",
"目录问题",
"修复建议",
"复检步骤"
],
"useCases": [
"发布修复",
"规范校验",
"批量整理"
],
"positiveExamples": [
"检查这个 skill 的 frontmatter 合不合规",
"修 requires 和 metadata"
],
"negativeExamples": [
"不要编造不存在的字段",
"不要忽略单行 JSON 约束"
],
"risk": "聚焦规范修复与校验。",
"tags": [
"skills",
"frontmatter",
"validation",
"openclaw"
]
}
FILE:resources/template.md
# Skill Frontmatter 医生 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 前言问题
- 待填写:围绕“前言问题”给出与 Skill Frontmatter 医生 场景相关的内容。
## metadata 问题
- 待填写:围绕“metadata 问题”给出与 Skill Frontmatter 医生 场景相关的内容。
## requires/install 问题
- 待填写:围绕“requires/install 问题”给出与 Skill Frontmatter 医生 场景相关的内容。
## 目录问题
- 待填写:围绕“目录问题”给出与 Skill Frontmatter 医生 场景相关的内容。
## 修复建议
- 待填写:围绕“修复建议”给出与 Skill Frontmatter 医生 场景相关的内容。
## 复检步骤
- 待填写:围绕“复检步骤”给出与 Skill Frontmatter 医生 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# Skill Frontmatter 医生 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 前言问题
- metadata 问题
- 复检步骤
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
为 Skill 生成高质量正例、反例和边界例,提升路由与可理解性。;use for skills, examples, routing workflows;do not use for 只给笼统例子, 忽视误触发场景.
---
name: skill-example-synthesizer
version: 1.0.0
description: "为 Skill 生成高质量正例、反例和边界例,提升路由与可理解性。;use for skills, examples, routing workflows;do not use for 只给笼统例子, 忽视误触发场景."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/skill-example-synthesizer
tags: [skills, examples, routing, quality]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧱","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# Skill 示例合成器
## 你是什么
你是“Skill 示例合成器”这个独立 Skill,负责:为 Skill 生成高质量正例、反例和边界例,提升路由与可理解性。
## Routing
### 适合使用的情况
- 给这个 skill 生成更好的 examples
- 覆盖正例和反例
- 输入通常包含:skill 描述、输入类型、禁用场景
- 优先产出:正例、反例、维护提示
### 不适合使用的情况
- 不要只给笼统例子
- 不要忽视误触发场景
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 正例
- 反例
- 边界例
- 常见误触发
- 文档放置建议
- 维护提示
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为 README 和 SKILL.md 配套内容。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# Skill 示例合成器
## 功能
为 Skill 生成高质量正例、反例和边界例,提升路由与可理解性。
## 适用场景
- 文档增强
- 路由调优
- 发布准备
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:skill 描述、输入类型、禁用场景
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为 README 和 SKILL.md 配套内容。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 给这个 skill 生成更好的 examples
- 覆盖正例和反例
## 输入输出示例
### 输入侧重点
- 正例
- 反例
- 边界例
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# Skill 示例合成器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# Skill 示例合成器 示例输入
目标:文档增强
输入类型:skill 描述、输入类型、禁用场景
## 背景
- 这是一个用于演示 Skill 示例合成器 的最小可复核样例。
- 希望产出与“正例 / 反例 / 维护提示”相关的结构化结果。
## 原始材料
- 主题:Skill 示例合成器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# Skill 示例合成器 示例输出
## 正例
- 这里是与“正例”相关的示例条目。
## 反例
- 这里是与“反例”相关的示例条目。
## 边界例
- 这里是与“边界例”相关的示例条目。
## 常见误触发
- 这里是与“常见误触发”相关的示例条目。
## 文档放置建议
- 这里是与“文档放置建议”相关的示例条目。
## 维护提示
- 这里是与“维护提示”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "skill-example-synthesizer",
"title": "Skill 示例合成器",
"category": "skills",
"categoryLabel": "Skill 生态与发布工程",
"mode": "structured_brief",
"summary": "为 Skill 生成高质量正例、反例和边界例,提升路由与可理解性。",
"inputHint": "skill 描述、输入类型、禁用场景",
"sections": [
"正例",
"反例",
"边界例",
"常见误触发",
"文档放置建议",
"维护提示"
],
"useCases": [
"文档增强",
"路由调优",
"发布准备"
],
"positiveExamples": [
"给这个 skill 生成更好的 examples",
"覆盖正例和反例"
],
"negativeExamples": [
"不要只给笼统例子",
"不要忽视误触发场景"
],
"risk": "适合作为 README 和 SKILL.md 配套内容。",
"tags": [
"skills",
"examples",
"routing",
"quality"
]
}
FILE:resources/template.md
# Skill 示例合成器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 正例
- 待填写:围绕“正例”给出与 Skill 示例合成器 场景相关的内容。
## 反例
- 待填写:围绕“反例”给出与 Skill 示例合成器 场景相关的内容。
## 边界例
- 待填写:围绕“边界例”给出与 Skill 示例合成器 场景相关的内容。
## 常见误触发
- 待填写:围绕“常见误触发”给出与 Skill 示例合成器 场景相关的内容。
## 文档放置建议
- 待填写:围绕“文档放置建议”给出与 Skill 示例合成器 场景相关的内容。
## 维护提示
- 待填写:围绕“维护提示”给出与 Skill 示例合成器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# Skill 示例合成器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 正例
- 反例
- 维护提示
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
在发布前检查目录中是否含秘钥、token、私有 URL、证书片段或凭证文件。;use for secrets, security, preflight workflows;do not use for 显示完整密钥值, 修改用户文件.
---
name: secret-exposure-gate
version: 1.0.0
description: "在发布前检查目录中是否含秘钥、token、私有 URL、证书片段或凭证文件。;use for secrets, security, preflight workflows;do not use for 显示完整密钥值, 修改用户文件."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/secret-exposure-gate
tags: [secrets, security, preflight, audit]
user-invocable: true
metadata: {"openclaw":{"emoji":"🔐","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 密钥暴露门禁器
## 你是什么
你是“密钥暴露门禁器”这个独立 Skill,负责:在发布前检查目录中是否含秘钥、token、私有 URL、证书片段或凭证文件。
## Routing
### 适合使用的情况
- 发布前帮我扫一遍目录里有没有密钥
- 检查 token 和私有 URL
- 输入通常包含:待发布目录路径
- 优先产出:扫描概览、疑似密钥、复检建议
### 不适合使用的情况
- 不要显示完整密钥值
- 不要修改用户文件
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 扫描概览
- 疑似密钥
- 高风险文件
- 误报说明
- 修复建议
- 复检建议
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为发布前门禁。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 密钥暴露门禁器
## 功能
在发布前检查目录中是否含秘钥、token、私有 URL、证书片段或凭证文件。
## 适用场景
- 发布前检查
- 仓库清洁
- 技能打包
## 推荐实现边界
- 模式:`pattern_audit` —— 扫描文本或目录中的高风险模式、差异和规则命中。
- 输入:待发布目录路径
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为发布前门禁。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 发布前帮我扫一遍目录里有没有密钥
- 检查 token 和私有 URL
## 输入输出示例
### 输入侧重点
- 扫描概览
- 疑似密钥
- 高风险文件
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 密钥暴露门禁器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 密钥暴露门禁器 示例输入
目标:发布前检查
输入类型:待发布目录路径
## 背景
- 这是一个用于演示 密钥暴露门禁器 的最小可复核样例。
- 希望产出与“扫描概览 / 疑似密钥 / 复检建议”相关的结构化结果。
## 原始材料
- 主题:密钥暴露门禁器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 密钥暴露门禁器 示例输出
## 扫描概览
- 这里是与“扫描概览”相关的示例条目。
## 疑似密钥
- 这里是与“疑似密钥”相关的示例条目。
## 高风险文件
- 这里是与“高风险文件”相关的示例条目。
## 误报说明
- 这里是与“误报说明”相关的示例条目。
## 修复建议
- 这里是与“修复建议”相关的示例条目。
## 复检建议
- 这里是与“复检建议”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "secret-exposure-gate",
"title": "密钥暴露门禁器",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "pattern_audit",
"summary": "在发布前检查目录中是否含秘钥、token、私有 URL、证书片段或凭证文件。",
"inputHint": "待发布目录路径",
"sections": [
"扫描概览",
"疑似密钥",
"高风险文件",
"误报说明",
"修复建议",
"复检建议"
],
"useCases": [
"发布前检查",
"仓库清洁",
"技能打包"
],
"positiveExamples": [
"发布前帮我扫一遍目录里有没有密钥",
"检查 token 和私有 URL"
],
"negativeExamples": [
"不要显示完整密钥值",
"不要修改用户文件"
],
"risk": "适合作为发布前门禁。",
"tags": [
"secrets",
"security",
"preflight",
"audit"
]
}
FILE:resources/template.md
# 密钥暴露门禁器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 扫描概览
- 待填写:围绕“扫描概览”给出与 密钥暴露门禁器 场景相关的内容。
## 疑似密钥
- 待填写:围绕“疑似密钥”给出与 密钥暴露门禁器 场景相关的内容。
## 高风险文件
- 待填写:围绕“高风险文件”给出与 密钥暴露门禁器 场景相关的内容。
## 误报说明
- 待填写:围绕“误报说明”给出与 密钥暴露门禁器 场景相关的内容。
## 修复建议
- 待填写:围绕“修复建议”给出与 密钥暴露门禁器 场景相关的内容。
## 复检建议
- 待填写:围绕“复检建议”给出与 密钥暴露门禁器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 密钥暴露门禁器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 扫描概览
- 疑似密钥
- 复检建议
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
把截图里的待办或灵感整理成任务、备注和优先级。;use for screenshots, tasks, capture workflows;do not use for 伪造截图内容, 替代 OCR 系统.
---
name: screenshot-to-task
version: 1.0.0
description: "把截图里的待办或灵感整理成任务、备注和优先级。;use for screenshots, tasks, capture workflows;do not use for 伪造截图内容, 替代 OCR 系统."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/screenshot-to-task
tags: [screenshots, tasks, capture, productivity]
user-invocable: true
metadata: {"openclaw":{"emoji":"🖼️","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 截图转任务整理器
## 你是什么
你是“截图转任务整理器”这个独立 Skill,负责:把截图里的待办或灵感整理成任务、备注和优先级。
## Routing
### 适合使用的情况
- 把这些截图内容整理成任务
- 帮我标优先级
- 输入通常包含:截图内容摘要、上下文、截止时间
- 优先产出:任务列表、备注、后续动作
### 不适合使用的情况
- 不要伪造截图内容
- 不要替代 OCR 系统
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 任务列表
- 备注
- 优先级
- 需要补充的信息
- 归档建议
- 后续动作
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为截图后整理流程。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 截图转任务整理器
## 功能
把截图里的待办或灵感整理成任务、备注和优先级。
## 适用场景
- 灵感收集
- 待办整理
- 会议截图整理
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:截图内容摘要、上下文、截止时间
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为截图后整理流程。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 把这些截图内容整理成任务
- 帮我标优先级
## 输入输出示例
### 输入侧重点
- 任务列表
- 备注
- 优先级
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 截图转任务整理器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 截图转任务整理器 示例输入
目标:灵感收集
输入类型:截图内容摘要、上下文、截止时间
## 背景
- 这是一个用于演示 截图转任务整理器 的最小可复核样例。
- 希望产出与“任务列表 / 备注 / 后续动作”相关的结构化结果。
## 原始材料
- 主题:截图转任务整理器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 截图转任务整理器 示例输出
## 任务列表
- 这里是与“任务列表”相关的示例条目。
## 备注
- 这里是与“备注”相关的示例条目。
## 优先级
- 这里是与“优先级”相关的示例条目。
## 需要补充的信息
- 这里是与“需要补充的信息”相关的示例条目。
## 归档建议
- 这里是与“归档建议”相关的示例条目。
## 后续动作
- 这里是与“后续动作”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "screenshot-to-task",
"title": "截图转任务整理器",
"category": "productivity",
"categoryLabel": "本地效率",
"mode": "structured_brief",
"summary": "把截图里的待办或灵感整理成任务、备注和优先级。",
"inputHint": "截图内容摘要、上下文、截止时间",
"sections": [
"任务列表",
"备注",
"优先级",
"需要补充的信息",
"归档建议",
"后续动作"
],
"useCases": [
"灵感收集",
"待办整理",
"会议截图整理"
],
"positiveExamples": [
"把这些截图内容整理成任务",
"帮我标优先级"
],
"negativeExamples": [
"不要伪造截图内容",
"不要替代 OCR 系统"
],
"risk": "适合作为截图后整理流程。",
"tags": [
"screenshots",
"tasks",
"capture",
"productivity"
]
}
FILE:resources/template.md
# 截图转任务整理器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 任务列表
- 待填写:围绕“任务列表”给出与 截图转任务整理器 场景相关的内容。
## 备注
- 待填写:围绕“备注”给出与 截图转任务整理器 场景相关的内容。
## 优先级
- 待填写:围绕“优先级”给出与 截图转任务整理器 场景相关的内容。
## 需要补充的信息
- 待填写:围绕“需要补充的信息”给出与 截图转任务整理器 场景相关的内容。
## 归档建议
- 待填写:围绕“归档建议”给出与 截图转任务整理器 场景相关的内容。
## 后续动作
- 待填写:围绕“后续动作”给出与 截图转任务整理器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 截图转任务整理器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 任务列表
- 备注
- 后续动作
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
把依赖清单或 SBOM 翻译成非技术可读的风险说明,按影响面排序。;use for sbom, dependencies, risk workflows;do not use for 伪造 CVE 状态, 替代专业漏洞扫描.
---
name: sbom-explainer
version: 1.0.0
description: "把依赖清单或 SBOM 翻译成非技术可读的风险说明,按影响面排序。;use for sbom, dependencies, risk workflows;do not use for 伪造 CVE 状态, 替代专业漏洞扫描."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/sbom-explainer
tags: [sbom, dependencies, risk, security]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧾","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# SBOM 说明官
## 你是什么
你是“SBOM 说明官”这个独立 Skill,负责:把依赖清单或 SBOM 翻译成非技术可读的风险说明,按影响面排序。
## Routing
### 适合使用的情况
- 把这份 SBOM 讲成人能看懂的话
- 按影响面排序风险
- 输入通常包含:SBOM、依赖列表、已知问题
- 优先产出:依赖概览、主要风险、沟通口径
### 不适合使用的情况
- 不要伪造 CVE 状态
- 不要替代专业漏洞扫描
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 依赖概览
- 主要风险
- 影响面
- 优先处理项
- 缓解建议
- 沟通口径
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为解释层而非扫描层。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# SBOM 说明官
## 功能
把依赖清单或 SBOM 翻译成非技术可读的风险说明,按影响面排序。
## 适用场景
- 管理层汇报
- 供应链风险沟通
- 发布说明
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:SBOM、依赖列表、已知问题
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为解释层而非扫描层。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 把这份 SBOM 讲成人能看懂的话
- 按影响面排序风险
## 输入输出示例
### 输入侧重点
- 依赖概览
- 主要风险
- 影响面
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# SBOM 说明官 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# SBOM 说明官 示例输入
目标:管理层汇报
输入类型:SBOM、依赖列表、已知问题
## 背景
- 这是一个用于演示 SBOM 说明官 的最小可复核样例。
- 希望产出与“依赖概览 / 主要风险 / 沟通口径”相关的结构化结果。
## 原始材料
- 主题:SBOM 说明官 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# SBOM 说明官 示例输出
## 依赖概览
- 这里是与“依赖概览”相关的示例条目。
## 主要风险
- 这里是与“主要风险”相关的示例条目。
## 影响面
- 这里是与“影响面”相关的示例条目。
## 优先处理项
- 这里是与“优先处理项”相关的示例条目。
## 缓解建议
- 这里是与“缓解建议”相关的示例条目。
## 沟通口径
- 这里是与“沟通口径”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "sbom-explainer",
"title": "SBOM 说明官",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "structured_brief",
"summary": "把依赖清单或 SBOM 翻译成非技术可读的风险说明,按影响面排序。",
"inputHint": "SBOM、依赖列表、已知问题",
"sections": [
"依赖概览",
"主要风险",
"影响面",
"优先处理项",
"缓解建议",
"沟通口径"
],
"useCases": [
"管理层汇报",
"供应链风险沟通",
"发布说明"
],
"positiveExamples": [
"把这份 SBOM 讲成人能看懂的话",
"按影响面排序风险"
],
"negativeExamples": [
"不要伪造 CVE 状态",
"不要替代专业漏洞扫描"
],
"risk": "适合作为解释层而非扫描层。",
"tags": [
"sbom",
"dependencies",
"risk",
"security"
]
}
FILE:resources/template.md
# SBOM 说明官 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 依赖概览
- 待填写:围绕“依赖概览”给出与 SBOM 说明官 场景相关的内容。
## 主要风险
- 待填写:围绕“主要风险”给出与 SBOM 说明官 场景相关的内容。
## 影响面
- 待填写:围绕“影响面”给出与 SBOM 说明官 场景相关的内容。
## 优先处理项
- 待填写:围绕“优先处理项”给出与 SBOM 说明官 场景相关的内容。
## 缓解建议
- 待填写:围绕“缓解建议”给出与 SBOM 说明官 场景相关的内容。
## 沟通口径
- 待填写:围绕“沟通口径”给出与 SBOM 说明官 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# SBOM 说明官 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 依赖概览
- 主要风险
- 沟通口径
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
在执行 shell 方案前检查危险模式,如 pipe-to-shell、覆盖式删除、危险重定向或混淆执行。;use for shell, security, command-review workflows;do not use for 提供攻击性命令, 帮用户绕过限制.
---
name: run-command-safety-check
version: 1.0.0
description: "在执行 shell 方案前检查危险模式,如 pipe-to-shell、覆盖式删除、危险重定向或混淆执行。;use for shell, security, command-review workflows;do not use for 提供攻击性命令, 帮用户绕过限制."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/run-command-safety-check
tags: [shell, security, command-review, safety]
user-invocable: true
metadata: {"openclaw":{"emoji":"🛑","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 命令执行安全检查官
## 你是什么
你是“命令执行安全检查官”这个独立 Skill,负责:在执行 shell 方案前检查危险模式,如 pipe-to-shell、覆盖式删除、危险重定向或混淆执行。
## Routing
### 适合使用的情况
- 检查这段 shell 命令安不安全
- 识别 pipe-to-shell 和 rm 风险
- 输入通常包含:命令文本、脚本文件或目录
- 优先产出:危险模式、中风险模式、最终建议
### 不适合使用的情况
- 不要提供攻击性命令
- 不要帮用户绕过限制
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 危险模式
- 中风险模式
- 背景说明
- 替代写法
- 人工确认项
- 最终建议
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 优先输出替代与审查意见,不执行命令。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 命令执行安全检查官
## 功能
在执行 shell 方案前检查危险模式,如 pipe-to-shell、覆盖式删除、危险重定向或混淆执行。
## 适用场景
- 脚本审查
- 命令复核
- CI 前检查
## 推荐实现边界
- 模式:`pattern_audit` —— 扫描文本或目录中的高风险模式、差异和规则命中。
- 输入:命令文本、脚本文件或目录
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:优先输出替代与审查意见,不执行命令。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 检查这段 shell 命令安不安全
- 识别 pipe-to-shell 和 rm 风险
## 输入输出示例
### 输入侧重点
- 危险模式
- 中风险模式
- 背景说明
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 命令执行安全检查官 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 命令执行安全检查官 示例输入
目标:脚本审查
输入类型:命令文本、脚本文件或目录
## 背景
- 这是一个用于演示 命令执行安全检查官 的最小可复核样例。
- 希望产出与“危险模式 / 中风险模式 / 最终建议”相关的结构化结果。
## 原始材料
- 主题:命令执行安全检查官 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 命令执行安全检查官 示例输出
## 危险模式
- 这里是与“危险模式”相关的示例条目。
## 中风险模式
- 这里是与“中风险模式”相关的示例条目。
## 背景说明
- 这里是与“背景说明”相关的示例条目。
## 替代写法
- 这里是与“替代写法”相关的示例条目。
## 人工确认项
- 这里是与“人工确认项”相关的示例条目。
## 最终建议
- 这里是与“最终建议”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "run-command-safety-check",
"title": "命令执行安全检查官",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "pattern_audit",
"summary": "在执行 shell 方案前检查危险模式,如 pipe-to-shell、覆盖式删除、危险重定向或混淆执行。",
"inputHint": "命令文本、脚本文件或目录",
"sections": [
"危险模式",
"中风险模式",
"背景说明",
"替代写法",
"人工确认项",
"最终建议"
],
"useCases": [
"脚本审查",
"命令复核",
"CI 前检查"
],
"positiveExamples": [
"检查这段 shell 命令安不安全",
"识别 pipe-to-shell 和 rm 风险"
],
"negativeExamples": [
"不要提供攻击性命令",
"不要帮用户绕过限制"
],
"risk": "优先输出替代与审查意见,不执行命令。",
"tags": [
"shell",
"security",
"command-review",
"safety"
]
}
FILE:resources/template.md
# 命令执行安全检查官 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 危险模式
- 待填写:围绕“危险模式”给出与 命令执行安全检查官 场景相关的内容。
## 中风险模式
- 待填写:围绕“中风险模式”给出与 命令执行安全检查官 场景相关的内容。
## 背景说明
- 待填写:围绕“背景说明”给出与 命令执行安全检查官 场景相关的内容。
## 替代写法
- 待填写:围绕“替代写法”给出与 命令执行安全检查官 场景相关的内容。
## 人工确认项
- 待填写:围绕“人工确认项”给出与 命令执行安全检查官 场景相关的内容。
## 最终建议
- 待填写:围绕“最终建议”给出与 命令执行安全检查官 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 命令执行安全检查官 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 危险模式
- 中风险模式
- 最终建议
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
根据 JD 生成面试题、评分维度、红旗项与面试记录模板。;use for recruiting, interview, hiring workflows;do not use for 生成歧视性问题, 替代最终录用决策.
---
name: recruiting-interview-kit
version: 1.0.0
description: "根据 JD 生成面试题、评分维度、红旗项与面试记录模板。;use for recruiting, interview, hiring workflows;do not use for 生成歧视性问题, 替代最终录用决策."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/recruiting-interview-kit
tags: [recruiting, interview, hiring, scorecard]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧑💼","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 招聘面试工具包
## 你是什么
你是“招聘面试工具包”这个独立 Skill,负责:根据 JD 生成面试题、评分维度、红旗项与面试记录模板。
## Routing
### 适合使用的情况
- 根据这个 JD 生成面试题和评分表
- 给我红旗项
- 输入通常包含:职位描述、级别、关键能力
- 优先产出:岗位理解、面试题库、候选人体验提醒
### 不适合使用的情况
- 不要生成歧视性问题
- 不要替代最终录用决策
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 岗位理解
- 面试题库
- 评分维度
- 红旗项
- 记录模板
- 候选人体验提醒
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合结构化招聘准备。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 招聘面试工具包
## 功能
根据 JD 生成面试题、评分维度、红旗项与面试记录模板。
## 适用场景
- 招聘准备
- 结构化面试
- 评分统一
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:职位描述、级别、关键能力
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合结构化招聘准备。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 根据这个 JD 生成面试题和评分表
- 给我红旗项
## 输入输出示例
### 输入侧重点
- 岗位理解
- 面试题库
- 评分维度
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 招聘面试工具包 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 招聘面试工具包 示例输入
目标:招聘准备
输入类型:职位描述、级别、关键能力
## 背景
- 这是一个用于演示 招聘面试工具包 的最小可复核样例。
- 希望产出与“岗位理解 / 面试题库 / 候选人体验提醒”相关的结构化结果。
## 原始材料
- 主题:招聘面试工具包 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 招聘面试工具包 示例输出
## 岗位理解
- 这里是与“岗位理解”相关的示例条目。
## 面试题库
- 这里是与“面试题库”相关的示例条目。
## 评分维度
- 这里是与“评分维度”相关的示例条目。
## 红旗项
- 这里是与“红旗项”相关的示例条目。
## 记录模板
- 这里是与“记录模板”相关的示例条目。
## 候选人体验提醒
- 这里是与“候选人体验提醒”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "recruiting-interview-kit",
"title": "招聘面试工具包",
"category": "vertical",
"categoryLabel": "垂直行业",
"mode": "structured_brief",
"summary": "根据 JD 生成面试题、评分维度、红旗项与面试记录模板。",
"inputHint": "职位描述、级别、关键能力",
"sections": [
"岗位理解",
"面试题库",
"评分维度",
"红旗项",
"记录模板",
"候选人体验提醒"
],
"useCases": [
"招聘准备",
"结构化面试",
"评分统一"
],
"positiveExamples": [
"根据这个 JD 生成面试题和评分表",
"给我红旗项"
],
"negativeExamples": [
"不要生成歧视性问题",
"不要替代最终录用决策"
],
"risk": "适合结构化招聘准备。",
"tags": [
"recruiting",
"interview",
"hiring",
"scorecard"
]
}
FILE:resources/template.md
# 招聘面试工具包 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 岗位理解
- 待填写:围绕“岗位理解”给出与 招聘面试工具包 场景相关的内容。
## 面试题库
- 待填写:围绕“面试题库”给出与 招聘面试工具包 场景相关的内容。
## 评分维度
- 待填写:围绕“评分维度”给出与 招聘面试工具包 场景相关的内容。
## 红旗项
- 待填写:围绕“红旗项”给出与 招聘面试工具包 场景相关的内容。
## 记录模板
- 待填写:围绕“记录模板”给出与 招聘面试工具包 场景相关的内容。
## 候选人体验提醒
- 待填写:围绕“候选人体验提醒”给出与 招聘面试工具包 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 招聘面试工具包 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 岗位理解
- 面试题库
- 候选人体验提醒
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
整理收据和报销资料,按周期、类别、凭证完整度做分组与缺失提醒。;use for receipts, expenses, finance-ops workflows;do not use for 替代正式财务报销系统, 生成虚假发票信息.
---
name: receipt-expense-sorter
version: 1.0.0
description: "整理收据和报销资料,按周期、类别、凭证完整度做分组与缺失提醒。;use for receipts, expenses, finance-ops workflows;do not use for 替代正式财务报销系统, 生成虚假发票信息."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/receipt-expense-sorter
tags: [receipts, expenses, finance-ops, organization]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧾","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 收据报销分类员
## 你是什么
你是“收据报销分类员”这个独立 Skill,负责:整理收据和报销资料,按周期、类别、凭证完整度做分组与缺失提醒。
## Routing
### 适合使用的情况
- 帮我整理报销资料
- 找缺失凭证和命名方式
- 输入通常包含:收据列表、日期、金额、类别
- 优先产出:按周期分组、按类别分组、注意事项
### 不适合使用的情况
- 不要替代正式财务报销系统
- 不要生成虚假发票信息
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 按周期分组
- 按类别分组
- 缺失凭证
- 命名建议
- 提交顺序
- 注意事项
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 仅做整理和提醒。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 收据报销分类员
## 功能
整理收据和报销资料,按周期、类别、凭证完整度做分组与缺失提醒。
## 适用场景
- 报销准备
- 资料归档
- 财务协作
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:收据列表、日期、金额、类别
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:仅做整理和提醒。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我整理报销资料
- 找缺失凭证和命名方式
## 输入输出示例
### 输入侧重点
- 按周期分组
- 按类别分组
- 缺失凭证
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 收据报销分类员 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 收据报销分类员 示例输入
目标:报销准备
输入类型:收据列表、日期、金额、类别
## 背景
- 这是一个用于演示 收据报销分类员 的最小可复核样例。
- 希望产出与“按周期分组 / 按类别分组 / 注意事项”相关的结构化结果。
## 原始材料
- 主题:收据报销分类员 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 收据报销分类员 示例输出
## 按周期分组
- 这里是与“按周期分组”相关的示例条目。
## 按类别分组
- 这里是与“按类别分组”相关的示例条目。
## 缺失凭证
- 这里是与“缺失凭证”相关的示例条目。
## 命名建议
- 这里是与“命名建议”相关的示例条目。
## 提交顺序
- 这里是与“提交顺序”相关的示例条目。
## 注意事项
- 这里是与“注意事项”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "receipt-expense-sorter",
"title": "收据报销分类员",
"category": "productivity",
"categoryLabel": "本地效率",
"mode": "structured_brief",
"summary": "整理收据和报销资料,按周期、类别、凭证完整度做分组与缺失提醒。",
"inputHint": "收据列表、日期、金额、类别",
"sections": [
"按周期分组",
"按类别分组",
"缺失凭证",
"命名建议",
"提交顺序",
"注意事项"
],
"useCases": [
"报销准备",
"资料归档",
"财务协作"
],
"positiveExamples": [
"帮我整理报销资料",
"找缺失凭证和命名方式"
],
"negativeExamples": [
"不要替代正式财务报销系统",
"不要生成虚假发票信息"
],
"risk": "仅做整理和提醒。",
"tags": [
"receipts",
"expenses",
"finance-ops",
"organization"
]
}
FILE:resources/template.md
# 收据报销分类员 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 按周期分组
- 待填写:围绕“按周期分组”给出与 收据报销分类员 场景相关的内容。
## 按类别分组
- 待填写:围绕“按类别分组”给出与 收据报销分类员 场景相关的内容。
## 缺失凭证
- 待填写:围绕“缺失凭证”给出与 收据报销分类员 场景相关的内容。
## 命名建议
- 待填写:围绕“命名建议”给出与 收据报销分类员 场景相关的内容。
## 提交顺序
- 待填写:围绕“提交顺序”给出与 收据报销分类员 场景相关的内容。
## 注意事项
- 待填写:围绕“注意事项”给出与 收据报销分类员 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 收据报销分类员 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 按周期分组
- 按类别分组
- 注意事项
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
为房产带看前整理买家画像、关注点、路线与现场提问清单。;use for real-estate, showing, brief workflows;do not use for 编造房源信息, 替代正式法律披露.
---
name: real-estate-showing-brief
version: 1.0.0
description: "为房产带看前整理买家画像、关注点、路线与现场提问清单。;use for real-estate, showing, brief workflows;do not use for 编造房源信息, 替代正式法律披露."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/real-estate-showing-brief
tags: [real-estate, showing, brief, buyers]
user-invocable: true
metadata: {"openclaw":{"emoji":"🏠","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 房产带看简报官
## 你是什么
你是“房产带看简报官”这个独立 Skill,负责:为房产带看前整理买家画像、关注点、路线与现场提问清单。
## Routing
### 适合使用的情况
- 帮我做一份带看前简报
- 按买家需求整理关注点
- 输入通常包含:买家需求、房源信息、看房安排
- 优先产出:买家画像、房源关注点、带看后记录
### 不适合使用的情况
- 不要编造房源信息
- 不要替代正式法律披露
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 买家画像
- 房源关注点
- 现场问题清单
- 路线与时间
- 风险提醒
- 带看后记录
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为现场沟通准备。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 房产带看简报官
## 功能
为房产带看前整理买家画像、关注点、路线与现场提问清单。
## 适用场景
- 房产带看
- 经纪人准备
- 客户沟通
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:买家需求、房源信息、看房安排
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为现场沟通准备。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我做一份带看前简报
- 按买家需求整理关注点
## 输入输出示例
### 输入侧重点
- 买家画像
- 房源关注点
- 现场问题清单
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 房产带看简报官 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 房产带看简报官 示例输入
目标:房产带看
输入类型:买家需求、房源信息、看房安排
## 背景
- 这是一个用于演示 房产带看简报官 的最小可复核样例。
- 希望产出与“买家画像 / 房源关注点 / 带看后记录”相关的结构化结果。
## 原始材料
- 主题:房产带看简报官 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 房产带看简报官 示例输出
## 买家画像
- 这里是与“买家画像”相关的示例条目。
## 房源关注点
- 这里是与“房源关注点”相关的示例条目。
## 现场问题清单
- 这里是与“现场问题清单”相关的示例条目。
## 路线与时间
- 这里是与“路线与时间”相关的示例条目。
## 风险提醒
- 这里是与“风险提醒”相关的示例条目。
## 带看后记录
- 这里是与“带看后记录”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "real-estate-showing-brief",
"title": "房产带看简报官",
"category": "vertical",
"categoryLabel": "垂直行业",
"mode": "structured_brief",
"summary": "为房产带看前整理买家画像、关注点、路线与现场提问清单。",
"inputHint": "买家需求、房源信息、看房安排",
"sections": [
"买家画像",
"房源关注点",
"现场问题清单",
"路线与时间",
"风险提醒",
"带看后记录"
],
"useCases": [
"房产带看",
"经纪人准备",
"客户沟通"
],
"positiveExamples": [
"帮我做一份带看前简报",
"按买家需求整理关注点"
],
"negativeExamples": [
"不要编造房源信息",
"不要替代正式法律披露"
],
"risk": "适合作为现场沟通准备。",
"tags": [
"real-estate",
"showing",
"brief",
"buyers"
]
}
FILE:resources/template.md
# 房产带看简报官 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 买家画像
- 待填写:围绕“买家画像”给出与 房产带看简报官 场景相关的内容。
## 房源关注点
- 待填写:围绕“房源关注点”给出与 房产带看简报官 场景相关的内容。
## 现场问题清单
- 待填写:围绕“现场问题清单”给出与 房产带看简报官 场景相关的内容。
## 路线与时间
- 待填写:围绕“路线与时间”给出与 房产带看简报官 场景相关的内容。
## 风险提醒
- 待填写:围绕“风险提醒”给出与 房产带看简报官 场景相关的内容。
## 带看后记录
- 待填写:围绕“带看后记录”给出与 房产带看简报官 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 房产带看简报官 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 买家画像
- 房源关注点
- 带看后记录
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
把需求拆成功能、异常、跨端、脏输入和恢复场景测试集。;use for qa, test-scenarios, edge-cases workflows;do not use for 宣称已经完成测试, 忽略关键用户路径.
---
name: qa-scenario-synthesizer
version: 1.0.0
description: "把需求拆成功能、异常、跨端、脏输入和恢复场景测试集。;use for qa, test-scenarios, edge-cases workflows;do not use for 宣称已经完成测试, 忽略关键用户路径."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/qa-scenario-synthesizer
tags: [qa, test-scenarios, edge-cases, quality]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧷","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 测试场景合成器
## 你是什么
你是“测试场景合成器”这个独立 Skill,负责:把需求拆成功能、异常、跨端、脏输入和恢复场景测试集。
## Routing
### 适合使用的情况
- 根据需求生成测试场景
- 补边界和异常路径
- 输入通常包含:需求规格、用户流程、限制条件
- 优先产出:主流程场景、异常场景、优先级
### 不适合使用的情况
- 不要宣称已经完成测试
- 不要忽略关键用户路径
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 主流程场景
- 异常场景
- 跨端场景
- 脏输入场景
- 恢复与重试
- 优先级
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 场景生成服务于人工测试设计。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 测试场景合成器
## 功能
把需求拆成功能、异常、跨端、脏输入和恢复场景测试集。
## 适用场景
- 测试设计
- 联调准备
- 验收覆盖
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:需求规格、用户流程、限制条件
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:场景生成服务于人工测试设计。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 根据需求生成测试场景
- 补边界和异常路径
## 输入输出示例
### 输入侧重点
- 主流程场景
- 异常场景
- 跨端场景
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 测试场景合成器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 测试场景合成器 示例输入
目标:测试设计
输入类型:需求规格、用户流程、限制条件
## 背景
- 这是一个用于演示 测试场景合成器 的最小可复核样例。
- 希望产出与“主流程场景 / 异常场景 / 优先级”相关的结构化结果。
## 原始材料
- 主题:测试场景合成器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 测试场景合成器 示例输出
## 主流程场景
- 这里是与“主流程场景”相关的示例条目。
## 异常场景
- 这里是与“异常场景”相关的示例条目。
## 跨端场景
- 这里是与“跨端场景”相关的示例条目。
## 脏输入场景
- 这里是与“脏输入场景”相关的示例条目。
## 恢复与重试
- 这里是与“恢复与重试”相关的示例条目。
## 优先级
- 这里是与“优先级”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "qa-scenario-synthesizer",
"title": "测试场景合成器",
"category": "engineering",
"categoryLabel": "研发与测试",
"mode": "structured_brief",
"summary": "把需求拆成功能、异常、跨端、脏输入和恢复场景测试集。",
"inputHint": "需求规格、用户流程、限制条件",
"sections": [
"主流程场景",
"异常场景",
"跨端场景",
"脏输入场景",
"恢复与重试",
"优先级"
],
"useCases": [
"测试设计",
"联调准备",
"验收覆盖"
],
"positiveExamples": [
"根据需求生成测试场景",
"补边界和异常路径"
],
"negativeExamples": [
"不要宣称已经完成测试",
"不要忽略关键用户路径"
],
"risk": "场景生成服务于人工测试设计。",
"tags": [
"qa",
"test-scenarios",
"edge-cases",
"quality"
]
}
FILE:resources/template.md
# 测试场景合成器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 主流程场景
- 待填写:围绕“主流程场景”给出与 测试场景合成器 场景相关的内容。
## 异常场景
- 待填写:围绕“异常场景”给出与 测试场景合成器 场景相关的内容。
## 跨端场景
- 待填写:围绕“跨端场景”给出与 测试场景合成器 场景相关的内容。
## 脏输入场景
- 待填写:围绕“脏输入场景”给出与 测试场景合成器 场景相关的内容。
## 恢复与重试
- 待填写:围绕“恢复与重试”给出与 测试场景合成器 场景相关的内容。
## 优先级
- 待填写:围绕“优先级”给出与 测试场景合成器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 测试场景合成器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 主流程场景
- 异常场景
- 优先级
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
对比多个方案文档,输出差异、隐含成本、风险与推荐结论。;use for proposal, comparison, decision workflows;do not use for 忽略用户给的约束, 伪造数据支持结论.
---
name: proposal-comparator
version: 1.0.0
description: "对比多个方案文档,输出差异、隐含成本、风险与推荐结论。;use for proposal, comparison, decision workflows;do not use for 忽略用户给的约束, 伪造数据支持结论."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/proposal-comparator
tags: [proposal, comparison, decision, analysis]
user-invocable: true
metadata: {"openclaw":{"emoji":"🧪","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 方案对比裁判
## 你是什么
你是“方案对比裁判”这个独立 Skill,负责:对比多个方案文档,输出差异、隐含成本、风险与推荐结论。
## Routing
### 适合使用的情况
- 比较 A/B 两个方案并给出取舍
- 指出隐藏成本和风险
- 输入通常包含:多个方案摘要、约束条件、评估标准
- 优先产出:对比维度、方案优劣、保留意见
### 不适合使用的情况
- 不要忽略用户给的约束
- 不要伪造数据支持结论
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 对比维度
- 方案优劣
- 隐藏成本
- 主要风险
- 推荐结论
- 保留意见
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 会显式分开事实比较与推断建议。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 方案对比裁判
## 功能
对比多个方案文档,输出差异、隐含成本、风险与推荐结论。
## 适用场景
- 方案评审
- 采购对比
- 技术选型
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:多个方案摘要、约束条件、评估标准
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:会显式分开事实比较与推断建议。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 比较 A/B 两个方案并给出取舍
- 指出隐藏成本和风险
## 输入输出示例
### 输入侧重点
- 对比维度
- 方案优劣
- 隐藏成本
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 方案对比裁判 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 方案对比裁判 示例输入
目标:方案评审
输入类型:多个方案摘要、约束条件、评估标准
## 背景
- 这是一个用于演示 方案对比裁判 的最小可复核样例。
- 希望产出与“对比维度 / 方案优劣 / 保留意见”相关的结构化结果。
## 原始材料
- 主题:方案对比裁判 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 方案对比裁判 示例输出
## 对比维度
- 这里是与“对比维度”相关的示例条目。
## 方案优劣
- 这里是与“方案优劣”相关的示例条目。
## 隐藏成本
- 这里是与“隐藏成本”相关的示例条目。
## 主要风险
- 这里是与“主要风险”相关的示例条目。
## 推荐结论
- 这里是与“推荐结论”相关的示例条目。
## 保留意见
- 这里是与“保留意见”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "proposal-comparator",
"title": "方案对比裁判",
"category": "docs",
"categoryLabel": "文档与知识",
"mode": "structured_brief",
"summary": "对比多个方案文档,输出差异、隐含成本、风险与推荐结论。",
"inputHint": "多个方案摘要、约束条件、评估标准",
"sections": [
"对比维度",
"方案优劣",
"隐藏成本",
"主要风险",
"推荐结论",
"保留意见"
],
"useCases": [
"方案评审",
"采购对比",
"技术选型"
],
"positiveExamples": [
"比较 A/B 两个方案并给出取舍",
"指出隐藏成本和风险"
],
"negativeExamples": [
"不要忽略用户给的约束",
"不要伪造数据支持结论"
],
"risk": "会显式分开事实比较与推断建议。",
"tags": [
"proposal",
"comparison",
"decision",
"analysis"
]
}
FILE:resources/template.md
# 方案对比裁判 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 对比维度
- 待填写:围绕“对比维度”给出与 方案对比裁判 场景相关的内容。
## 方案优劣
- 待填写:围绕“方案优劣”给出与 方案对比裁判 场景相关的内容。
## 隐藏成本
- 待填写:围绕“隐藏成本”给出与 方案对比裁判 场景相关的内容。
## 主要风险
- 待填写:围绕“主要风险”给出与 方案对比裁判 场景相关的内容。
## 推荐结论
- 待填写:围绕“推荐结论”给出与 方案对比裁判 场景相关的内容。
## 保留意见
- 待填写:围绕“保留意见”给出与 方案对比裁判 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 方案对比裁判 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 对比维度
- 方案优劣
- 保留意见
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
审查 prompt、Skill 文案和说明中是否泄漏密钥、路径、内部规则或高风险指令。;use for prompt, security, audit workflows;do not use for 把扫描到的密钥原文再次扩散, 输出可利用攻击步骤.
---
name: prompt-leak-auditor
version: 1.0.0
description: "审查 prompt、Skill 文案和说明中是否泄漏密钥、路径、内部规则或高风险指令。;use for prompt, security, audit workflows;do not use for 把扫描到的密钥原文再次扩散, 输出可利用攻击步骤."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/prompt-leak-auditor
tags: [prompt, security, audit, leaks]
user-invocable: true
metadata: {"openclaw":{"emoji":"🕵️","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 提示泄漏审计器
## 你是什么
你是“提示泄漏审计器”这个独立 Skill,负责:审查 prompt、Skill 文案和说明中是否泄漏密钥、路径、内部规则或高风险指令。
## Routing
### 适合使用的情况
- 检查这些 prompt 有没有泄漏风险
- 扫描 skill 文案中的敏感内容
- 输入通常包含:提示词、SKILL.md、README 或目录
- 优先产出:扫描范围、疑似泄漏、后续治理
### 不适合使用的情况
- 不要把扫描到的密钥原文再次扩散
- 不要输出可利用攻击步骤
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 扫描范围
- 疑似泄漏
- 高风险模式
- 建议修复
- 人工复核点
- 后续治理
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 报告默认掩码高敏感内容。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 提示泄漏审计器
## 功能
审查 prompt、Skill 文案和说明中是否泄漏密钥、路径、内部规则或高风险指令。
## 适用场景
- 发布前审计
- 内部提示检查
- 安全自检
## 推荐实现边界
- 模式:`pattern_audit` —— 扫描文本或目录中的高风险模式、差异和规则命中。
- 输入:提示词、SKILL.md、README 或目录
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:报告默认掩码高敏感内容。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 检查这些 prompt 有没有泄漏风险
- 扫描 skill 文案中的敏感内容
## 输入输出示例
### 输入侧重点
- 扫描范围
- 疑似泄漏
- 高风险模式
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 提示泄漏审计器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 提示泄漏审计器 示例输入
目标:发布前审计
输入类型:提示词、SKILL.md、README 或目录
## 背景
- 这是一个用于演示 提示泄漏审计器 的最小可复核样例。
- 希望产出与“扫描范围 / 疑似泄漏 / 后续治理”相关的结构化结果。
## 原始材料
- 主题:提示泄漏审计器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 提示泄漏审计器 示例输出
## 扫描范围
- 这里是与“扫描范围”相关的示例条目。
## 疑似泄漏
- 这里是与“疑似泄漏”相关的示例条目。
## 高风险模式
- 这里是与“高风险模式”相关的示例条目。
## 建议修复
- 这里是与“建议修复”相关的示例条目。
## 人工复核点
- 这里是与“人工复核点”相关的示例条目。
## 后续治理
- 这里是与“后续治理”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:out.md
# 提示泄漏审计器 模式扫描
扫描目标:`/mnt/data/clawhub-100-innovative-skills-bundle/prompt-leak-auditor/examples/example-input.md`
## 发现结果
- 未命中内置高风险模式。
## 扫描范围
- 围绕“扫描范围”给出人工复核和修复建议。
## 疑似泄漏
- 围绕“疑似泄漏”给出人工复核和修复建议。
## 高风险模式
- 围绕“高风险模式”给出人工复核和修复建议。
## 建议修复
- 围绕“建议修复”给出人工复核和修复建议。
## 人工复核点
- 围绕“人工复核点”给出人工复核和修复建议。
## 后续治理
- 围绕“后续治理”给出人工复核和修复建议。
FILE:resources/spec.json
{
"slug": "prompt-leak-auditor",
"title": "提示泄漏审计器",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "pattern_audit",
"summary": "审查 prompt、Skill 文案和说明中是否泄漏密钥、路径、内部规则或高风险指令。",
"inputHint": "提示词、SKILL.md、README 或目录",
"sections": [
"扫描范围",
"疑似泄漏",
"高风险模式",
"建议修复",
"人工复核点",
"后续治理"
],
"useCases": [
"发布前审计",
"内部提示检查",
"安全自检"
],
"positiveExamples": [
"检查这些 prompt 有没有泄漏风险",
"扫描 skill 文案中的敏感内容"
],
"negativeExamples": [
"不要把扫描到的密钥原文再次扩散",
"不要输出可利用攻击步骤"
],
"risk": "报告默认掩码高敏感内容。",
"tags": [
"prompt",
"security",
"audit",
"leaks"
]
}
FILE:resources/template.md
# 提示泄漏审计器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 扫描范围
- 待填写:围绕“扫描范围”给出与 提示泄漏审计器 场景相关的内容。
## 疑似泄漏
- 待填写:围绕“疑似泄漏”给出与 提示泄漏审计器 场景相关的内容。
## 高风险模式
- 待填写:围绕“高风险模式”给出与 提示泄漏审计器 场景相关的内容。
## 建议修复
- 待填写:围绕“建议修复”给出与 提示泄漏审计器 场景相关的内容。
## 人工复核点
- 待填写:围绕“人工复核点”给出与 提示泄漏审计器 场景相关的内容。
## 后续治理
- 待填写:围绕“后续治理”给出与 提示泄漏审计器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 提示泄漏审计器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 扫描范围
- 疑似泄漏
- 后续治理
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
把零散需求、聊天和会议内容整理成项目任务书、边界、验收标准和风险清单。;use for project-brief, requirements, scope workflows;do not use for 生成法律合同, 替代正式需求审批.
---
name: project-brief-writer
version: 1.0.0
description: "把零散需求、聊天和会议内容整理成项目任务书、边界、验收标准和风险清单。;use for project-brief, requirements, scope workflows;do not use for 生成法律合同, 替代正式需求审批."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/project-brief-writer
tags: [project-brief, requirements, scope, acceptance]
user-invocable: true
metadata: {"openclaw":{"emoji":"🗂️","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 项目任务书作者
## 你是什么
你是“项目任务书作者”这个独立 Skill,负责:把零散需求、聊天和会议内容整理成项目任务书、边界、验收标准和风险清单。
## Routing
### 适合使用的情况
- 把零散需求整理成项目 brief
- 补齐验收标准和边界
- 输入通常包含:需求点、目标、约束、相关聊天
- 优先产出:项目目标、范围与非范围、里程碑
### 不适合使用的情况
- 不要用来生成法律合同
- 不要替代正式需求审批
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 项目目标
- 范围与非范围
- 关键需求
- 验收标准
- 依赖与风险
- 里程碑
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 会尽量显式标注假设和未确认点。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 项目任务书作者
## 功能
把零散需求、聊天和会议内容整理成项目任务书、边界、验收标准和风险清单。
## 适用场景
- 立项文档
- 需求整理
- 交付定义
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:需求点、目标、约束、相关聊天
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:会尽量显式标注假设和未确认点。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 把零散需求整理成项目 brief
- 补齐验收标准和边界
## 输入输出示例
### 输入侧重点
- 项目目标
- 范围与非范围
- 关键需求
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 项目任务书作者 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 项目任务书作者 示例输入
目标:立项文档
输入类型:需求点、目标、约束、相关聊天
## 背景
- 这是一个用于演示 项目任务书作者 的最小可复核样例。
- 希望产出与“项目目标 / 范围与非范围 / 里程碑”相关的结构化结果。
## 原始材料
- 主题:项目任务书作者 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 项目任务书作者 示例输出
## 项目目标
- 这里是与“项目目标”相关的示例条目。
## 范围与非范围
- 这里是与“范围与非范围”相关的示例条目。
## 关键需求
- 这里是与“关键需求”相关的示例条目。
## 验收标准
- 这里是与“验收标准”相关的示例条目。
## 依赖与风险
- 这里是与“依赖与风险”相关的示例条目。
## 里程碑
- 这里是与“里程碑”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "project-brief-writer",
"title": "项目任务书作者",
"category": "meeting",
"categoryLabel": "会议与执行",
"mode": "structured_brief",
"summary": "把零散需求、聊天和会议内容整理成项目任务书、边界、验收标准和风险清单。",
"inputHint": "需求点、目标、约束、相关聊天",
"sections": [
"项目目标",
"范围与非范围",
"关键需求",
"验收标准",
"依赖与风险",
"里程碑"
],
"useCases": [
"立项文档",
"需求整理",
"交付定义"
],
"positiveExamples": [
"把零散需求整理成项目 brief",
"补齐验收标准和边界"
],
"negativeExamples": [
"不要用来生成法律合同",
"不要替代正式需求审批"
],
"risk": "会尽量显式标注假设和未确认点。",
"tags": [
"project-brief",
"requirements",
"scope",
"acceptance"
]
}
FILE:resources/template.md
# 项目任务书作者 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 项目目标
- 待填写:围绕“项目目标”给出与 项目任务书作者 场景相关的内容。
## 范围与非范围
- 待填写:围绕“范围与非范围”给出与 项目任务书作者 场景相关的内容。
## 关键需求
- 待填写:围绕“关键需求”给出与 项目任务书作者 场景相关的内容。
## 验收标准
- 待填写:围绕“验收标准”给出与 项目任务书作者 场景相关的内容。
## 依赖与风险
- 待填写:围绕“依赖与风险”给出与 项目任务书作者 场景相关的内容。
## 里程碑
- 待填写:围绕“里程碑”给出与 项目任务书作者 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 项目任务书作者 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 项目目标
- 范围与非范围
- 里程碑
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
面对多个目标、有限资源与冲突时,输出排序、牺牲说明、边界与不做清单。;use for priority, tradeoff, planning workflows;do not use for 替代人事绩效决策, 伪造资源承诺.
---
name: priority-conflict-resolver
version: 1.0.0
description: "面对多个目标、有限资源与冲突时,输出排序、牺牲说明、边界与不做清单。;use for priority, tradeoff, planning workflows;do not use for 替代人事绩效决策, 伪造资源承诺."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/priority-conflict-resolver
tags: [priority, tradeoff, planning, strategy]
user-invocable: true
metadata: {"openclaw":{"emoji":"⚖️","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 优先级冲突化解器
## 你是什么
你是“优先级冲突化解器”这个独立 Skill,负责:面对多个目标、有限资源与冲突时,输出排序、牺牲说明、边界与不做清单。
## Routing
### 适合使用的情况
- 帮我处理多个项目的优先级冲突
- 生成不做清单和取舍说明
- 输入通常包含:目标列表、资源约束、冲突描述
- 优先产出:任务排序、排序依据、重新评估触发器
### 不适合使用的情况
- 不要替代人事绩效决策
- 不要伪造资源承诺
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 任务排序
- 排序依据
- 必须牺牲的事项
- 沟通口径
- 复盘点
- 重新评估触发器
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 排序结果是辅助建议,最终取舍需人工确认。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 优先级冲突化解器
## 功能
面对多个目标、有限资源与冲突时,输出排序、牺牲说明、边界与不做清单。
## 适用场景
- 排期冲突
- 资源有限
- 范围控制
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:目标列表、资源约束、冲突描述
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:排序结果是辅助建议,最终取舍需人工确认。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我处理多个项目的优先级冲突
- 生成不做清单和取舍说明
## 输入输出示例
### 输入侧重点
- 任务排序
- 排序依据
- 必须牺牲的事项
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 优先级冲突化解器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 优先级冲突化解器 示例输入
目标:排期冲突
输入类型:目标列表、资源约束、冲突描述
## 背景
- 这是一个用于演示 优先级冲突化解器 的最小可复核样例。
- 希望产出与“任务排序 / 排序依据 / 重新评估触发器”相关的结构化结果。
## 原始材料
- 主题:优先级冲突化解器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 优先级冲突化解器 示例输出
## 任务排序
- 这里是与“任务排序”相关的示例条目。
## 排序依据
- 这里是与“排序依据”相关的示例条目。
## 必须牺牲的事项
- 这里是与“必须牺牲的事项”相关的示例条目。
## 沟通口径
- 这里是与“沟通口径”相关的示例条目。
## 复盘点
- 这里是与“复盘点”相关的示例条目。
## 重新评估触发器
- 这里是与“重新评估触发器”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "priority-conflict-resolver",
"title": "优先级冲突化解器",
"category": "meeting",
"categoryLabel": "会议与执行",
"mode": "structured_brief",
"summary": "面对多个目标、有限资源与冲突时,输出排序、牺牲说明、边界与不做清单。",
"inputHint": "目标列表、资源约束、冲突描述",
"sections": [
"任务排序",
"排序依据",
"必须牺牲的事项",
"沟通口径",
"复盘点",
"重新评估触发器"
],
"useCases": [
"排期冲突",
"资源有限",
"范围控制"
],
"positiveExamples": [
"帮我处理多个项目的优先级冲突",
"生成不做清单和取舍说明"
],
"negativeExamples": [
"不要替代人事绩效决策",
"不要伪造资源承诺"
],
"risk": "排序结果是辅助建议,最终取舍需人工确认。",
"tags": [
"priority",
"tradeoff",
"planning",
"strategy"
]
}
FILE:resources/template.md
# 优先级冲突化解器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 任务排序
- 待填写:围绕“任务排序”给出与 优先级冲突化解器 场景相关的内容。
## 排序依据
- 待填写:围绕“排序依据”给出与 优先级冲突化解器 场景相关的内容。
## 必须牺牲的事项
- 待填写:围绕“必须牺牲的事项”给出与 优先级冲突化解器 场景相关的内容。
## 沟通口径
- 待填写:围绕“沟通口径”给出与 优先级冲突化解器 场景相关的内容。
## 复盘点
- 待填写:围绕“复盘点”给出与 优先级冲突化解器 场景相关的内容。
## 重新评估触发器
- 待填写:围绕“重新评估触发器”给出与 优先级冲突化解器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 优先级冲突化解器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 任务排序
- 排序依据
- 重新评估触发器
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
比较新旧制度或政策差异,指出业务影响、流程影响和需更新的操作手册。;use for policy, diff, governance workflows;do not use for 给法律定性结论, 忽略边缘影响人群.
---
name: policy-delta-watcher
version: 1.0.0
description: "比较新旧制度或政策差异,指出业务影响、流程影响和需更新的操作手册。;use for policy, diff, governance workflows;do not use for 给法律定性结论, 忽略边缘影响人群."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/policy-delta-watcher
tags: [policy, diff, governance, compliance]
user-invocable: true
metadata: {"openclaw":{"emoji":"📜","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 制度差异观察员
## 你是什么
你是“制度差异观察员”这个独立 Skill,负责:比较新旧制度或政策差异,指出业务影响、流程影响和需更新的操作手册。
## Routing
### 适合使用的情况
- 比较新旧制度有哪些差异
- 说明会影响哪些流程
- 输入通常包含:旧版政策、新版政策、受影响团队
- 优先产出:主要变化、业务影响、待确认问题
### 不适合使用的情况
- 不要给法律定性结论
- 不要忽略边缘影响人群
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 主要变化
- 业务影响
- 流程影响
- 需更新文档
- 培训建议
- 待确认问题
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为政策变更说明底稿。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 制度差异观察员
## 功能
比较新旧制度或政策差异,指出业务影响、流程影响和需更新的操作手册。
## 适用场景
- 制度升级
- 内控变更
- 流程调整
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:旧版政策、新版政策、受影响团队
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为政策变更说明底稿。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 比较新旧制度有哪些差异
- 说明会影响哪些流程
## 输入输出示例
### 输入侧重点
- 主要变化
- 业务影响
- 流程影响
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 制度差异观察员 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 制度差异观察员 示例输入
目标:制度升级
输入类型:旧版政策、新版政策、受影响团队
## 背景
- 这是一个用于演示 制度差异观察员 的最小可复核样例。
- 希望产出与“主要变化 / 业务影响 / 待确认问题”相关的结构化结果。
## 原始材料
- 主题:制度差异观察员 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 制度差异观察员 示例输出
## 主要变化
- 这里是与“主要变化”相关的示例条目。
## 业务影响
- 这里是与“业务影响”相关的示例条目。
## 流程影响
- 这里是与“流程影响”相关的示例条目。
## 需更新文档
- 这里是与“需更新文档”相关的示例条目。
## 培训建议
- 这里是与“培训建议”相关的示例条目。
## 待确认问题
- 这里是与“待确认问题”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "policy-delta-watcher",
"title": "制度差异观察员",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "structured_brief",
"summary": "比较新旧制度或政策差异,指出业务影响、流程影响和需更新的操作手册。",
"inputHint": "旧版政策、新版政策、受影响团队",
"sections": [
"主要变化",
"业务影响",
"流程影响",
"需更新文档",
"培训建议",
"待确认问题"
],
"useCases": [
"制度升级",
"内控变更",
"流程调整"
],
"positiveExamples": [
"比较新旧制度有哪些差异",
"说明会影响哪些流程"
],
"negativeExamples": [
"不要给法律定性结论",
"不要忽略边缘影响人群"
],
"risk": "适合作为政策变更说明底稿。",
"tags": [
"policy",
"diff",
"governance",
"compliance"
]
}
FILE:resources/template.md
# 制度差异观察员 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 主要变化
- 待填写:围绕“主要变化”给出与 制度差异观察员 场景相关的内容。
## 业务影响
- 待填写:围绕“业务影响”给出与 制度差异观察员 场景相关的内容。
## 流程影响
- 待填写:围绕“流程影响”给出与 制度差异观察员 场景相关的内容。
## 需更新文档
- 待填写:围绕“需更新文档”给出与 制度差异观察员 场景相关的内容。
## 培训建议
- 待填写:围绕“培训建议”给出与 制度差异观察员 场景相关的内容。
## 待确认问题
- 待填写:围绕“待确认问题”给出与 制度差异观察员 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 制度差异观察员 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 主要变化
- 业务影响
- 待确认问题
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
从选题到上线整理播客生产流程,生成 show notes、标题、剪辑要点与发布清单。;use for podcast, production, content workflows;do not use for 虚构嘉宾观点, 公开未授权片段.
---
name: podcast-production-ops
version: 1.0.0
description: "从选题到上线整理播客生产流程,生成 show notes、标题、剪辑要点与发布清单。;use for podcast, production, content workflows;do not use for 虚构嘉宾观点, 公开未授权片段."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/podcast-production-ops
tags: [podcast, production, content, ops]
user-invocable: true
metadata: {"openclaw":{"emoji":"🎙️","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 播客制作运营台
## 你是什么
你是“播客制作运营台”这个独立 Skill,负责:从选题到上线整理播客生产流程,生成 show notes、标题、剪辑要点与发布清单。
## Routing
### 适合使用的情况
- 把这期播客整理成完整生产包
- 给我标题和 show notes
- 输入通常包含:节目主题、嘉宾摘要、录音要点
- 优先产出:选题摘要、标题备选、复用素材
### 不适合使用的情况
- 不要虚构嘉宾观点
- 不要公开未授权片段
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 选题摘要
- 标题备选
- 剪辑要点
- show notes
- 发布清单
- 复用素材
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为音频后期与发布协作骨架。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 播客制作运营台
## 功能
从选题到上线整理播客生产流程,生成 show notes、标题、剪辑要点与发布清单。
## 适用场景
- 播客制作
- 内容团队协作
- 发布流程
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:节目主题、嘉宾摘要、录音要点
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为音频后期与发布协作骨架。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 把这期播客整理成完整生产包
- 给我标题和 show notes
## 输入输出示例
### 输入侧重点
- 选题摘要
- 标题备选
- 剪辑要点
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 播客制作运营台 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 播客制作运营台 示例输入
目标:播客制作
输入类型:节目主题、嘉宾摘要、录音要点
## 背景
- 这是一个用于演示 播客制作运营台 的最小可复核样例。
- 希望产出与“选题摘要 / 标题备选 / 复用素材”相关的结构化结果。
## 原始材料
- 主题:播客制作运营台 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 播客制作运营台 示例输出
## 选题摘要
- 这里是与“选题摘要”相关的示例条目。
## 标题备选
- 这里是与“标题备选”相关的示例条目。
## 剪辑要点
- 这里是与“剪辑要点”相关的示例条目。
## show notes
- 这里是与“show notes”相关的示例条目。
## 发布清单
- 这里是与“发布清单”相关的示例条目。
## 复用素材
- 这里是与“复用素材”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "podcast-production-ops",
"title": "播客制作运营台",
"category": "vertical",
"categoryLabel": "垂直行业",
"mode": "structured_brief",
"summary": "从选题到上线整理播客生产流程,生成 show notes、标题、剪辑要点与发布清单。",
"inputHint": "节目主题、嘉宾摘要、录音要点",
"sections": [
"选题摘要",
"标题备选",
"剪辑要点",
"show notes",
"发布清单",
"复用素材"
],
"useCases": [
"播客制作",
"内容团队协作",
"发布流程"
],
"positiveExamples": [
"把这期播客整理成完整生产包",
"给我标题和 show notes"
],
"negativeExamples": [
"不要虚构嘉宾观点",
"不要公开未授权片段"
],
"risk": "适合作为音频后期与发布协作骨架。",
"tags": [
"podcast",
"production",
"content",
"ops"
]
}
FILE:resources/template.md
# 播客制作运营台 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 选题摘要
- 待填写:围绕“选题摘要”给出与 播客制作运营台 场景相关的内容。
## 标题备选
- 待填写:围绕“标题备选”给出与 播客制作运营台 场景相关的内容。
## 剪辑要点
- 待填写:围绕“剪辑要点”给出与 播客制作运营台 场景相关的内容。
## show notes
- 待填写:围绕“show notes”给出与 播客制作运营台 场景相关的内容。
## 发布清单
- 待填写:围绕“发布清单”给出与 播客制作运营台 场景相关的内容。
## 复用素材
- 待填写:围绕“复用素材”给出与 播客制作运营台 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 播客制作运营台 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 选题摘要
- 标题备选
- 复用素材
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
扫描个人待办、会议和消息摘要,找即将到期事项与时间冲突。;use for deadlines, calendar, tasks workflows;do not use for 自动改日历, 伪造截止时间.
---
name: personal-deadline-radar
version: 1.0.0
description: "扫描个人待办、会议和消息摘要,找即将到期事项与时间冲突。;use for deadlines, calendar, tasks workflows;do not use for 自动改日历, 伪造截止时间."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/personal-deadline-radar
tags: [deadlines, calendar, tasks, productivity]
user-invocable: true
metadata: {"openclaw":{"emoji":"📡","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 个人截止日雷达
## 你是什么
你是“个人截止日雷达”这个独立 Skill,负责:扫描个人待办、会议和消息摘要,找即将到期事项与时间冲突。
## Routing
### 适合使用的情况
- 帮我找最近的 deadline 风险
- 识别时间冲突
- 输入通常包含:待办清单、日历摘要、截止日
- 优先产出:临近截止事项、冲突时间段、今日清单
### 不适合使用的情况
- 不要自动改日历
- 不要伪造截止时间
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 临近截止事项
- 冲突时间段
- 高风险遗漏
- 建议重排
- 可延后事项
- 今日清单
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为个人排程辅助。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 个人截止日雷达
## 功能
扫描个人待办、会议和消息摘要,找即将到期事项与时间冲突。
## 适用场景
- 时间管理
- 冲突预警
- 周计划
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:待办清单、日历摘要、截止日
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为个人排程辅助。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我找最近的 deadline 风险
- 识别时间冲突
## 输入输出示例
### 输入侧重点
- 临近截止事项
- 冲突时间段
- 高风险遗漏
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 个人截止日雷达 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 个人截止日雷达 示例输入
目标:时间管理
输入类型:待办清单、日历摘要、截止日
## 背景
- 这是一个用于演示 个人截止日雷达 的最小可复核样例。
- 希望产出与“临近截止事项 / 冲突时间段 / 今日清单”相关的结构化结果。
## 原始材料
- 主题:个人截止日雷达 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 个人截止日雷达 示例输出
## 临近截止事项
- 这里是与“临近截止事项”相关的示例条目。
## 冲突时间段
- 这里是与“冲突时间段”相关的示例条目。
## 高风险遗漏
- 这里是与“高风险遗漏”相关的示例条目。
## 建议重排
- 这里是与“建议重排”相关的示例条目。
## 可延后事项
- 这里是与“可延后事项”相关的示例条目。
## 今日清单
- 这里是与“今日清单”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "personal-deadline-radar",
"title": "个人截止日雷达",
"category": "productivity",
"categoryLabel": "本地效率",
"mode": "structured_brief",
"summary": "扫描个人待办、会议和消息摘要,找即将到期事项与时间冲突。",
"inputHint": "待办清单、日历摘要、截止日",
"sections": [
"临近截止事项",
"冲突时间段",
"高风险遗漏",
"建议重排",
"可延后事项",
"今日清单"
],
"useCases": [
"时间管理",
"冲突预警",
"周计划"
],
"positiveExamples": [
"帮我找最近的 deadline 风险",
"识别时间冲突"
],
"negativeExamples": [
"不要自动改日历",
"不要伪造截止时间"
],
"risk": "适合作为个人排程辅助。",
"tags": [
"deadlines",
"calendar",
"tasks",
"productivity"
]
}
FILE:resources/template.md
# 个人截止日雷达 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 临近截止事项
- 待填写:围绕“临近截止事项”给出与 个人截止日雷达 场景相关的内容。
## 冲突时间段
- 待填写:围绕“冲突时间段”给出与 个人截止日雷达 场景相关的内容。
## 高风险遗漏
- 待填写:围绕“高风险遗漏”给出与 个人截止日雷达 场景相关的内容。
## 建议重排
- 待填写:围绕“建议重排”给出与 个人截止日雷达 场景相关的内容。
## 可延后事项
- 待填写:围绕“可延后事项”给出与 个人截止日雷达 场景相关的内容。
## 今日清单
- 待填写:围绕“今日清单”给出与 个人截止日雷达 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 个人截止日雷达 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 临近截止事项
- 冲突时间段
- 今日清单
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
梳理某个 Skill、脚本或工作流需要的权限,并提出最小权限替代方案。;use for permissions, least-privilege, security workflows;do not use for 绕过系统安全控制, 生成提权方法.
---
name: permission-footprint-reviewer
version: 1.0.0
description: "梳理某个 Skill、脚本或工作流需要的权限,并提出最小权限替代方案。;use for permissions, least-privilege, security workflows;do not use for 绕过系统安全控制, 生成提权方法."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/permission-footprint-reviewer
tags: [permissions, least-privilege, security, audit]
user-invocable: true
metadata: {"openclaw":{"emoji":"🪪","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 权限足迹审查员
## 你是什么
你是“权限足迹审查员”这个独立 Skill,负责:梳理某个 Skill、脚本或工作流需要的权限,并提出最小权限替代方案。
## Routing
### 适合使用的情况
- 审查这个脚本需要哪些权限
- 给我最小权限方案
- 输入通常包含:脚本说明、权限需求、运行环境
- 优先产出:当前权限足迹、用途说明、复核清单
### 不适合使用的情况
- 不要绕过系统安全控制
- 不要生成提权方法
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 当前权限足迹
- 用途说明
- 过度权限
- 最小权限替代
- 隔离建议
- 复核清单
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 只做审计和最小化建议。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 权限足迹审查员
## 功能
梳理某个 Skill、脚本或工作流需要的权限,并提出最小权限替代方案。
## 适用场景
- 权限评审
- 技能审查
- 自动化治理
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:脚本说明、权限需求、运行环境
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:只做审计和最小化建议。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 审查这个脚本需要哪些权限
- 给我最小权限方案
## 输入输出示例
### 输入侧重点
- 当前权限足迹
- 用途说明
- 过度权限
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 权限足迹审查员 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 权限足迹审查员 示例输入
目标:权限评审
输入类型:脚本说明、权限需求、运行环境
## 背景
- 这是一个用于演示 权限足迹审查员 的最小可复核样例。
- 希望产出与“当前权限足迹 / 用途说明 / 复核清单”相关的结构化结果。
## 原始材料
- 主题:权限足迹审查员 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 权限足迹审查员 示例输出
## 当前权限足迹
- 这里是与“当前权限足迹”相关的示例条目。
## 用途说明
- 这里是与“用途说明”相关的示例条目。
## 过度权限
- 这里是与“过度权限”相关的示例条目。
## 最小权限替代
- 这里是与“最小权限替代”相关的示例条目。
## 隔离建议
- 这里是与“隔离建议”相关的示例条目。
## 复核清单
- 这里是与“复核清单”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "permission-footprint-reviewer",
"title": "权限足迹审查员",
"category": "security",
"categoryLabel": "安全与治理",
"mode": "structured_brief",
"summary": "梳理某个 Skill、脚本或工作流需要的权限,并提出最小权限替代方案。",
"inputHint": "脚本说明、权限需求、运行环境",
"sections": [
"当前权限足迹",
"用途说明",
"过度权限",
"最小权限替代",
"隔离建议",
"复核清单"
],
"useCases": [
"权限评审",
"技能审查",
"自动化治理"
],
"positiveExamples": [
"审查这个脚本需要哪些权限",
"给我最小权限方案"
],
"negativeExamples": [
"不要绕过系统安全控制",
"不要生成提权方法"
],
"risk": "只做审计和最小化建议。",
"tags": [
"permissions",
"least-privilege",
"security",
"audit"
]
}
FILE:resources/template.md
# 权限足迹审查员 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 当前权限足迹
- 待填写:围绕“当前权限足迹”给出与 权限足迹审查员 场景相关的内容。
## 用途说明
- 待填写:围绕“用途说明”给出与 权限足迹审查员 场景相关的内容。
## 过度权限
- 待填写:围绕“过度权限”给出与 权限足迹审查员 场景相关的内容。
## 最小权限替代
- 待填写:围绕“最小权限替代”给出与 权限足迹审查员 场景相关的内容。
## 隔离建议
- 待填写:围绕“隔离建议”给出与 权限足迹审查员 场景相关的内容。
## 复核清单
- 待填写:围绕“复核清单”给出与 权限足迹审查员 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 权限足迹审查员 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 当前权限足迹
- 用途说明
- 复核清单
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
为渠道或合作伙伴生成 enablement 包,按伙伴类型拆分版本与 FAQ。;use for partner, enablement, channel workflows;do not use for 公开内部敏感政策, 替代正式合作协议.
---
name: partner-enable-kit
version: 1.0.0
description: "为渠道或合作伙伴生成 enablement 包,按伙伴类型拆分版本与 FAQ。;use for partner, enablement, channel workflows;do not use for 公开内部敏感政策, 替代正式合作协议."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/partner-enable-kit
tags: [partner, enablement, channel, training]
user-invocable: true
metadata: {"openclaw":{"emoji":"🎒","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 伙伴赋能包组装师
## 你是什么
你是“伙伴赋能包组装师”这个独立 Skill,负责:为渠道或合作伙伴生成 enablement 包,按伙伴类型拆分版本与 FAQ。
## Routing
### 适合使用的情况
- 为渠道伙伴做一套 enablement 包
- 按伙伴类型拆版本
- 输入通常包含:伙伴类型、产品范围、目标场景
- 优先产出:伙伴画像、必备资料、更新节奏
### 不适合使用的情况
- 不要公开内部敏感政策
- 不要替代正式合作协议
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 伙伴画像
- 必备资料
- FAQ
- 常见阻塞
- 版本差异
- 更新节奏
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 输出以资料包结构和文案为主。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 伙伴赋能包组装师
## 功能
为渠道或合作伙伴生成 enablement 包,按伙伴类型拆分版本与 FAQ。
## 适用场景
- 伙伴培训
- 渠道支持
- 销售 enablement
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:伙伴类型、产品范围、目标场景
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:输出以资料包结构和文案为主。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 为渠道伙伴做一套 enablement 包
- 按伙伴类型拆版本
## 输入输出示例
### 输入侧重点
- 伙伴画像
- 必备资料
- FAQ
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 伙伴赋能包组装师 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 伙伴赋能包组装师 示例输入
目标:伙伴培训
输入类型:伙伴类型、产品范围、目标场景
## 背景
- 这是一个用于演示 伙伴赋能包组装师 的最小可复核样例。
- 希望产出与“伙伴画像 / 必备资料 / 更新节奏”相关的结构化结果。
## 原始材料
- 主题:伙伴赋能包组装师 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 伙伴赋能包组装师 示例输出
## 伙伴画像
- 这里是与“伙伴画像”相关的示例条目。
## 必备资料
- 这里是与“必备资料”相关的示例条目。
## FAQ
- 这里是与“FAQ”相关的示例条目。
## 常见阻塞
- 这里是与“常见阻塞”相关的示例条目。
## 版本差异
- 这里是与“版本差异”相关的示例条目。
## 更新节奏
- 这里是与“更新节奏”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "partner-enable-kit",
"title": "伙伴赋能包组装师",
"category": "success",
"categoryLabel": "客户成功与协作",
"mode": "structured_brief",
"summary": "为渠道或合作伙伴生成 enablement 包,按伙伴类型拆分版本与 FAQ。",
"inputHint": "伙伴类型、产品范围、目标场景",
"sections": [
"伙伴画像",
"必备资料",
"FAQ",
"常见阻塞",
"版本差异",
"更新节奏"
],
"useCases": [
"伙伴培训",
"渠道支持",
"销售 enablement"
],
"positiveExamples": [
"为渠道伙伴做一套 enablement 包",
"按伙伴类型拆版本"
],
"negativeExamples": [
"不要公开内部敏感政策",
"不要替代正式合作协议"
],
"risk": "输出以资料包结构和文案为主。",
"tags": [
"partner",
"enablement",
"channel",
"training"
]
}
FILE:resources/template.md
# 伙伴赋能包组装师 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 伙伴画像
- 待填写:围绕“伙伴画像”给出与 伙伴赋能包组装师 场景相关的内容。
## 必备资料
- 待填写:围绕“必备资料”给出与 伙伴赋能包组装师 场景相关的内容。
## FAQ
- 待填写:围绕“FAQ”给出与 伙伴赋能包组装师 场景相关的内容。
## 常见阻塞
- 待填写:围绕“常见阻塞”给出与 伙伴赋能包组装师 场景相关的内容。
## 版本差异
- 待填写:围绕“版本差异”给出与 伙伴赋能包组装师 场景相关的内容。
## 更新节奏
- 待填写:围绕“更新节奏”给出与 伙伴赋能包组装师 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 伙伴赋能包组装师 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 伙伴画像
- 必备资料
- 更新节奏
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
设计客户或员工 onboarding 路径,按第 1 天、第 7 天、第 30 天拆解。;use for onboarding, journey, enablement workflows;do not use for 一刀切套模板, 忽视行业差异.
---
name: onboarding-journey-designer
version: 1.0.0
description: "设计客户或员工 onboarding 路径,按第 1 天、第 7 天、第 30 天拆解。;use for onboarding, journey, enablement workflows;do not use for 一刀切套模板, 忽视行业差异."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/onboarding-journey-designer
tags: [onboarding, journey, enablement, success]
user-invocable: true
metadata: {"openclaw":{"emoji":"🚀","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 上手旅程设计师
## 你是什么
你是“上手旅程设计师”这个独立 Skill,负责:设计客户或员工 onboarding 路径,按第 1 天、第 7 天、第 30 天拆解。
## Routing
### 适合使用的情况
- 设计客户 onboarding 路线
- 按第 1/7/30 天拆任务
- 输入通常包含:目标人群、核心动作、成功标准
- 优先产出:第1天、第7天、衡量指标
### 不适合使用的情况
- 不要一刀切套模板
- 不要忽视行业差异
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 第1天
- 第7天
- 第30天
- 关键里程碑
- 阻塞预警
- 衡量指标
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 适合作为旅程蓝图,需要结合具体产品调整。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 上手旅程设计师
## 功能
设计客户或员工 onboarding 路径,按第 1 天、第 7 天、第 30 天拆解。
## 适用场景
- 客户启用
- 员工入职
- 新用户引导
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:目标人群、核心动作、成功标准
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:适合作为旅程蓝图,需要结合具体产品调整。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 设计客户 onboarding 路线
- 按第 1/7/30 天拆任务
## 输入输出示例
### 输入侧重点
- 第1天
- 第7天
- 第30天
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 上手旅程设计师 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 上手旅程设计师 示例输入
目标:客户启用
输入类型:目标人群、核心动作、成功标准
## 背景
- 这是一个用于演示 上手旅程设计师 的最小可复核样例。
- 希望产出与“第1天 / 第7天 / 衡量指标”相关的结构化结果。
## 原始材料
- 主题:上手旅程设计师 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 上手旅程设计师 示例输出
## 第1天
- 这里是与“第1天”相关的示例条目。
## 第7天
- 这里是与“第7天”相关的示例条目。
## 第30天
- 这里是与“第30天”相关的示例条目。
## 关键里程碑
- 这里是与“关键里程碑”相关的示例条目。
## 阻塞预警
- 这里是与“阻塞预警”相关的示例条目。
## 衡量指标
- 这里是与“衡量指标”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "onboarding-journey-designer",
"title": "上手旅程设计师",
"category": "success",
"categoryLabel": "客户成功与协作",
"mode": "structured_brief",
"summary": "设计客户或员工 onboarding 路径,按第 1 天、第 7 天、第 30 天拆解。",
"inputHint": "目标人群、核心动作、成功标准",
"sections": [
"第1天",
"第7天",
"第30天",
"关键里程碑",
"阻塞预警",
"衡量指标"
],
"useCases": [
"客户启用",
"员工入职",
"新用户引导"
],
"positiveExamples": [
"设计客户 onboarding 路线",
"按第 1/7/30 天拆任务"
],
"negativeExamples": [
"不要一刀切套模板",
"不要忽视行业差异"
],
"risk": "适合作为旅程蓝图,需要结合具体产品调整。",
"tags": [
"onboarding",
"journey",
"enablement",
"success"
]
}
FILE:resources/template.md
# 上手旅程设计师 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 第1天
- 待填写:围绕“第1天”给出与 上手旅程设计师 场景相关的内容。
## 第7天
- 待填写:围绕“第7天”给出与 上手旅程设计师 场景相关的内容。
## 第30天
- 待填写:围绕“第30天”给出与 上手旅程设计师 场景相关的内容。
## 关键里程碑
- 待填写:围绕“关键里程碑”给出与 上手旅程设计师 场景相关的内容。
## 阻塞预警
- 待填写:围绕“阻塞预警”给出与 上手旅程设计师 场景相关的内容。
## 衡量指标
- 待填写:围绕“衡量指标”给出与 上手旅程设计师 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 上手旅程设计师 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 第1天
- 第7天
- 衡量指标
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰
为公益项目生成 grant proposal 大纲,连接目标、预算、影响指标与证据。;use for nonprofit, grant, proposal workflows;do not use for 伪造受益人数, 替代正式合规审核.
---
name: nonprofit-grant-outline
version: 1.0.0
description: "为公益项目生成 grant proposal 大纲,连接目标、预算、影响指标与证据。;use for nonprofit, grant, proposal workflows;do not use for 伪造受益人数, 替代正式合规审核."
author: OpenClaw Skill Bundle
homepage: https://example.invalid/skills/nonprofit-grant-outline
tags: [nonprofit, grant, proposal, impact]
user-invocable: true
metadata: {"openclaw":{"emoji":"🌱","requires":{"bins":["python3"]},"os":["darwin","linux","win32"]}}
---
# 公益资助申请大纲器
## 你是什么
你是“公益资助申请大纲器”这个独立 Skill,负责:为公益项目生成 grant proposal 大纲,连接目标、预算、影响指标与证据。
## Routing
### 适合使用的情况
- 帮我写一份公益 grant proposal 大纲
- 把预算和影响指标连起来
- 输入通常包含:项目目标、受益人、预算、证据
- 优先产出:项目摘要、问题定义、证据与风险
### 不适合使用的情况
- 不要伪造受益人数
- 不要替代正式合规审核
- 如果用户想直接执行外部系统写入、发送、删除、发布、变更配置,先明确边界,再只给审阅版内容或 dry-run 方案。
## 工作规则
1. 先把用户提供的信息重组成任务书,再输出结构化结果。
2. 缺信息时,优先显式列出“待确认项”,而不是直接编造。
3. 默认先给“可审阅草案”,再给“可执行清单”。
4. 遇到高风险、隐私、权限或合规问题,必须加上边界说明。
5. 如运行环境允许 shell / exec,可使用:
- `python3 "{baseDir}/scripts/run.py" --input <输入文件> --output <输出文件>`
6. 如当前环境不能执行脚本,仍要基于 `{baseDir}/resources/template.md` 与 `{baseDir}/resources/spec.json` 的结构直接产出文本。
## 标准输出结构
请尽量按以下结构组织结果:
- 项目摘要
- 问题定义
- 行动方案
- 预算框架
- 影响指标
- 证据与风险
## 本地资源
- 规范文件:`{baseDir}/resources/spec.json`
- 输出模板:`{baseDir}/resources/template.md`
- 示例输入输出:`{baseDir}/examples/`
- 冒烟测试:`{baseDir}/tests/smoke-test.md`
## 安全边界
- 输出为申请大纲草案。
- 默认只读、可审计、可回滚。
- 不执行高风险命令,不隐藏依赖,不伪造事实或结果。
FILE:README.md
# 公益资助申请大纲器
## 功能
为公益项目生成 grant proposal 大纲,连接目标、预算、影响指标与证据。
## 适用场景
- 基金申请
- 公益项目设计
- 项目传播
## 推荐实现边界
- 模式:`structured_brief` —— 把输入材料整理成结构化 Markdown 成品。
- 输入:项目目标、受益人、预算、证据
- 输出:以 Markdown 为主,强调可审阅、可追踪、可补充。
- 风险控制:输出为申请大纲草案。
## 安装要求
- `python3`
- 无额外三方依赖
- 建议在支持 `skills/` 目录加载的 OpenClaw 工作区中使用
## 目录结构
- `SKILL.md`:Skill 说明与路由规则
- `README.md`:功能、场景、安装、用法和风险说明
- `SELF_CHECK.md`:本 Skill 的规范与质量自检
- `scripts/run.py`:本地可执行脚本,负责生成或审计结果
- `resources/spec.json`:结构化配置,驱动脚本与模板
- `resources/template.md`:输出模板
- `examples/example-input.md`:示例输入
- `examples/example-output.md`:示例输出
- `tests/smoke-test.md`:冒烟测试步骤
## 触发示例
- 帮我写一份公益 grant proposal 大纲
- 把预算和影响指标连起来
## 输入输出示例
### 输入侧重点
- 项目摘要
- 问题定义
- 行动方案
### 本地命令
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
### 预期输出
- 结构化 Markdown
- 明确的待确认项
- 面向当前场景的下一步建议
## 脚本参数
```text
--input 输入文件或目录
--output 输出文件,默认 stdout
--format markdown/json,默认 markdown
--limit 限制扫描或摘要数量
--dry-run 仅分析不写文件
```
## 常见问题
**问:这个 Skill 会直接修改外部系统吗?** 不会,默认只生成草案、清单或只读审计结果。
**问:没有 shell/exec 工具还能用吗?** 可以,Skill 会直接按模板产出文本结果。
**问:脚本依赖什么?** 只依赖 `python3` 和 Python 标准库。
## 风险提示
- 仅使用本地输入内容,不联网补事实。
- 默认不删除、不写外部系统、不发消息、不发布。
- 若输入含个人信息或敏感材料,建议先脱敏再处理。
FILE:SELF_CHECK.md
# 公益资助申请大纲器 自检
| 维度 | 结果 | 说明 |
|---|---|---|
| frontmatter | 通过 | 包含 name/description/version/metadata,metadata 为单行 JSON。 |
| 目录 | 通过 | 包含 SKILL.md、README.md、SELF_CHECK.md、scripts、resources、examples、tests。 |
| 脚本 | 通过 | `scripts/run.py` 可执行、带参数解析、异常处理、无 TODO。 |
| 资源引用 | 通过 | 脚本和 SKILL.md 都引用 `resources/spec.json` 与 `resources/template.md`。 |
| 依赖 | 通过 | 仅依赖 python3 和标准库,已在 metadata.openclaw.requires.bins 声明。 |
| 安全 | 通过 | 默认只读/审阅模式,不包含 curl|bash、base64 混淆执行、远程灌脚本。 |
| 热门度 | 通过 | 场景属于高频工作流,门槛低,可二次定制。 |
| 可维护性 | 通过 | 结构统一,资源驱动,便于版本升级和批量修订。 |
## 评分
- 综合评分:96/100
- 扣分点:暂无阻断项;后续可按真实用户反馈再细化例子和模板。
## 审计结论
- 本 Skill 不直接执行高风险系统变更。
- 本 Skill 适合作为 ClawHub 发布前的低风险、可审计成品。
FILE:examples/example-input.md
# 公益资助申请大纲器 示例输入
目标:基金申请
输入类型:项目目标、受益人、预算、证据
## 背景
- 这是一个用于演示 公益资助申请大纲器 的最小可复核样例。
- 希望产出与“项目摘要 / 问题定义 / 证据与风险”相关的结构化结果。
## 原始材料
- 主题:公益资助申请大纲器 场景演示
- 约束:时间有限,需要先产出审阅版,再决定是否落地。
- 风险:不允许编造事实,不允许直接执行高风险动作。
## 额外要求
- 使用清晰标题。
- 标出待确认项。
- 给出下一步建议。
FILE:examples/example-output.md
# 公益资助申请大纲器 示例输出
## 项目摘要
- 这里是与“项目摘要”相关的示例条目。
## 问题定义
- 这里是与“问题定义”相关的示例条目。
## 行动方案
- 这里是与“行动方案”相关的示例条目。
## 预算框架
- 这里是与“预算框架”相关的示例条目。
## 影响指标
- 这里是与“影响指标”相关的示例条目。
## 证据与风险
- 这里是与“证据与风险”相关的示例条目。
## 待确认项
- 这里列出仍需用户补充的信息。
## 下一步
- 在用户确认后,再进入执行或二次加工。
FILE:resources/spec.json
{
"slug": "nonprofit-grant-outline",
"title": "公益资助申请大纲器",
"category": "vertical",
"categoryLabel": "垂直行业",
"mode": "structured_brief",
"summary": "为公益项目生成 grant proposal 大纲,连接目标、预算、影响指标与证据。",
"inputHint": "项目目标、受益人、预算、证据",
"sections": [
"项目摘要",
"问题定义",
"行动方案",
"预算框架",
"影响指标",
"证据与风险"
],
"useCases": [
"基金申请",
"公益项目设计",
"项目传播"
],
"positiveExamples": [
"帮我写一份公益 grant proposal 大纲",
"把预算和影响指标连起来"
],
"negativeExamples": [
"不要伪造受益人数",
"不要替代正式合规审核"
],
"risk": "输出为申请大纲草案。",
"tags": [
"nonprofit",
"grant",
"proposal",
"impact"
]
}
FILE:resources/template.md
# 公益资助申请大纲器 输出模板
> 本模板由脚本和 Skill 共用。若无法自动执行,请按下面结构手工填写。
## 项目摘要
- 待填写:围绕“项目摘要”给出与 公益资助申请大纲器 场景相关的内容。
## 问题定义
- 待填写:围绕“问题定义”给出与 公益资助申请大纲器 场景相关的内容。
## 行动方案
- 待填写:围绕“行动方案”给出与 公益资助申请大纲器 场景相关的内容。
## 预算框架
- 待填写:围绕“预算框架”给出与 公益资助申请大纲器 场景相关的内容。
## 影响指标
- 待填写:围绕“影响指标”给出与 公益资助申请大纲器 场景相关的内容。
## 证据与风险
- 待填写:围绕“证据与风险”给出与 公益资助申请大纲器 场景相关的内容。
## 待确认项
- 如输入不足,请在这里明确列出缺失信息。
FILE:scripts/run.py
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import sys
from pathlib import Path
from collections import Counter
BASE_DIR = Path(__file__).resolve().parents[1]
SPEC_PATH = BASE_DIR / "resources" / "spec.json"
TEMPLATE_PATH = BASE_DIR / "resources" / "template.md"
def fail(message: str, code: int = 2) -> int:
print(f"ERROR: {message}", file=sys.stderr)
return code
def load_spec() -> dict:
try:
return json.loads(SPEC_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
raise SystemExit(fail(f"Missing spec file: {SPEC_PATH}"))
except json.JSONDecodeError as exc:
raise SystemExit(fail(f"Invalid JSON in {SPEC_PATH}: {exc}"))
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return path.read_text(encoding="utf-8", errors="replace")
def list_text_files(root: Path, limit: int = 50):
results = []
for path in root.rglob("*"):
if len(results) >= limit:
break
if path.is_file():
if path.suffix.lower() in {".md",".txt",".json",".yaml",".yml",".py",".js",".ts",".csv",".tsv",".sh"}:
results.append(path)
return results
def make_structured_report(spec: dict, input_text: str) -> str:
title = spec["title"]
summary = spec["summary"]
sections = spec["sections"]
bullets = [line.strip("- ").strip() for line in input_text.splitlines() if line.strip()]
bullets = bullets[:18]
out = [f"# {title} 结果", "", f"> 模式:{spec['mode']}", f"> 摘要:{summary}", ""]
for idx, section in enumerate(sections):
out.append(f"## {section}")
if bullets:
selected = bullets[idx::max(1, len(sections))][:3]
for item in selected:
out.append(f"- {item}")
else:
out.append("- 输入材料不足,请补充更具体的原始信息。")
out.append("")
out.append("## 待确认项")
out.append(f"- 请补充:{spec.get('inputHint', '更完整的输入材料')}")
out.append("")
out.append("## 下一步")
out.append("- 先审阅上述结构,再决定是否进入执行、发送、发布或系统变更。")
return "\n".join(out).strip() + "\n"
def directory_report(spec: dict, root: Path, limit: int) -> str:
files = list_text_files(root, limit=limit)
ext_counter = Counter(p.suffix.lower() or "<none>" for p in files)
headings = []
for p in files[: min(10, len(files))]:
if p.suffix.lower() == ".md":
text = read_text(p)
for line in text.splitlines():
if line.startswith("#"):
headings.append((p.name, line.strip()))
if len(headings) >= 12:
break
if len(headings) >= 12:
break
out = [f"# {spec['title']} 扫描报告", "", f"扫描目录:`{root}`", f"文本文件样本数:{len(files)}", ""]
out.append("## 目录概览")
for p in files[:15]:
out.append(f"- {p.relative_to(root)}")
out.append("")
out.append("## 扩展名分布")
for ext, cnt in ext_counter.most_common():
out.append(f"- {ext}: {cnt}")
out.append("")
out.append("## 标题样本")
if headings:
for fname, heading in headings:
out.append(f"- {fname}: {heading}")
else:
out.append("- 未发现 Markdown 标题。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 基于目录和文件样本,围绕“{section}”给出人工审阅意见。")
out.append("")
return "\n".join(out).strip() + "\n"
def csv_report(spec: dict, path: Path, limit: int) -> str:
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
rows = []
with path.open("r", encoding="utf-8", errors="replace", newline="") as fh:
reader = csv.DictReader(fh, delimiter=delimiter)
for idx, row in enumerate(reader):
rows.append(row)
if idx + 1 >= limit:
break
if not rows:
return make_structured_report(spec, "未读取到数据行。")
fieldnames = list(rows[0].keys())
out = [f"# {spec['title']} 数据报告", "", f"文件:`{path}`", f"采样行数:{len(rows)}", ""]
out.append("## 字段概览")
for field in fieldnames:
values = [r.get(field, "") for r in rows]
non_empty = [v for v in values if str(v).strip()]
unique = len(set(non_empty))
out.append(f"- {field}: 非空 {len(non_empty)}/{len(rows)},唯一值约 {unique}")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 结合字段概览与样本,围绕“{section}”补充判断。")
out.append("")
return "\n".join(out).strip() + "\n"
PATTERNS = {
"curl_pipe_bash": r"curl\s+[^|]+\|\s*(bash|sh)",
"dangerous_rm": r"\brm\s+-rf\s+(/|\*|~|\.{1,2})",
"base64_exec": r"base64\s+(-d|--decode).+\|\s*(bash|sh|python)",
"secret_like": r"(api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{8,}",
"private_url": r"https?://[^/\s]+/(admin|internal|private|secret)",
}
def pattern_report(spec: dict, path: Path, limit: int) -> str:
targets = [path] if path.is_file() else list_text_files(path, limit=limit)
findings = []
for target in targets:
text = read_text(target)
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, text, flags=re.IGNORECASE):
snippet = match.group(0)
if "secret_like" == name:
snippet = re.sub(r"([A-Za-z0-9_\-]{4})[A-Za-z0-9_\-]+", r"\1***", snippet)
findings.append((str(target), name, snippet[:160]))
if len(findings) >= limit:
break
if len(findings) >= limit:
break
if len(findings) >= limit:
break
out = [f"# {spec['title']} 模式扫描", "", f"扫描目标:`{path}`", ""]
out.append("## 发现结果")
if findings:
for target, name, snippet in findings:
out.append(f"- [{name}] {target}: `{snippet}`")
else:
out.append("- 未命中内置高风险模式。")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出人工复核和修复建议。")
out.append("")
return "\n".join(out).strip() + "\n"
def parse_frontmatter(path: Path):
text = read_text(path)
if not text.startswith("---\n"):
return None, "SKILL.md 缺少前置 frontmatter"
parts = text.split("\n---\n", 1)
if len(parts) < 2:
return None, "frontmatter 未正确闭合"
front = parts[0].splitlines()[1:]
data = {}
for line in front:
if not line.strip() or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip()
return data, None
def skill_audit(spec: dict, path: Path, limit: int) -> str:
required = [
"SKILL.md",
"README.md",
"SELF_CHECK.md",
"scripts/run.py",
"resources/spec.json",
"resources/template.md",
"examples/example-input.md",
"tests/smoke-test.md",
]
out = [f"# {spec['title']} 规范检查", "", f"检查目标:`{path}`", ""]
out.append("## 文件完整性")
for rel in required:
target = path / rel
out.append(f"- {rel}: {'OK' if target.exists() else 'MISSING'}")
out.append("")
skill_md = path / "SKILL.md"
if skill_md.exists():
data, err = parse_frontmatter(skill_md)
out.append("## Frontmatter")
if err:
out.append(f"- 错误:{err}")
else:
for key in ("name","description","version","metadata"):
out.append(f"- {key}: {'OK' if key in data else 'MISSING'}")
metadata_value = data.get("metadata", "")
if metadata_value:
try:
json.loads(metadata_value)
out.append("- metadata JSON: OK")
except Exception as exc:
out.append(f"- metadata JSON: INVALID ({exc})")
out.append("")
for section in spec["sections"]:
out.append(f"## {section}")
out.append(f"- 围绕“{section}”给出修复建议或复检动作。")
out.append("")
return "\n".join(out).strip() + "\n"
def build_report(spec: dict, source: Path, limit: int) -> str:
mode = spec["mode"]
if mode == "structured_brief":
text = read_text(source) if source.exists() and source.is_file() else str(source)
return make_structured_report(spec, text)
if mode == "directory_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"目录不存在:{source}")
return directory_report(spec, source, limit)
if mode == "csv_audit":
if not source.exists() or not source.is_file():
return make_structured_report(spec, f"文件不存在:{source}")
return csv_report(spec, source, limit)
if mode == "pattern_audit":
if not source.exists():
return make_structured_report(spec, f"目标不存在:{source}")
return pattern_report(spec, source, limit)
if mode == "skill_audit":
if not source.exists() or not source.is_dir():
return make_structured_report(spec, f"Skill 目录不存在:{source}")
return skill_audit(spec, source, limit)
return make_structured_report(spec, f"未知模式:{mode}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run the local support script for this Skill.")
parser.add_argument("--input", required=True, help="Input file, directory, or inline string.")
parser.add_argument("--output", help="Write output to a file instead of stdout.")
parser.add_argument("--format", choices=["markdown","json"], default="markdown", help="Output format.")
parser.add_argument("--limit", type=int, default=50, help="Limit sample size or findings.")
parser.add_argument("--dry-run", action="store_true", help="Analyze only and skip file writing.")
args = parser.parse_args()
spec = load_spec()
source = Path(args.input).expanduser()
if source.exists():
report = build_report(spec, source, args.limit)
else:
if spec["mode"] in {"directory_audit","csv_audit","pattern_audit","skill_audit"}:
return fail(f"Input path does not exist: {source}")
report = build_report(spec, Path(args.input), args.limit)
if args.format == "json":
payload = {"skill": spec["slug"], "mode": spec["mode"], "report": report}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
else:
rendered = report
if args.dry_run or not args.output:
print(rendered)
return 0
output_path = Path(args.output).expanduser()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(rendered, encoding="utf-8")
print(f"Wrote output to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:tests/smoke-test.md
# 公益资助申请大纲器 冒烟测试
## 测试目标
验证目录完整、脚本可运行、模板可生成、异常输入可被正确处理。
## 步骤
1. 检查目录包含必需文件:
- `SKILL.md`
- `README.md`
- `SELF_CHECK.md`
- `scripts/run.py`
- `resources/spec.json`
- `resources/template.md`
- `examples/example-input.md`
2. 执行:
```bash
python3 scripts/run.py --input examples/example-input.md --output out.md
```
3. 观察 `out.md` 是否成功生成,且至少包含以下章节:
- 项目摘要
- 问题定义
- 证据与风险
4. 执行异常路径:
```bash
python3 scripts/run.py --input does-not-exist.md
```
5. 预期:
- 正常路径返回 0 并生成结构化内容
- 异常路径返回非 0,并输出可读错误信息
## 通过标准
- 脚本可执行
- 输出结构正确
- 错误处理清晰