强化市场校验与索引元数据

合并市场索引、分类、i18n 校验、skill-creator 工具和已修改 skill 版本号更新。
This commit is contained in:
2026-07-07 21:14:31 +08:00
committed by GitHub
parent b273a1008a
commit a4718379d9
34 changed files with 1129 additions and 218 deletions

View File

@@ -5,7 +5,7 @@ description: >-
frontmatter 元数据 + L0/L1/L2 分层内容 + 脚本/参考/资产)和 Claude Code
基础格式。Use when 用户要求创建新技能、更新已有技能、或将经验封装为可复用
的技能包。
version: 1.0.3
version: 1.0.4
type: meta
risk_level: low
status: enabled
@@ -39,7 +39,7 @@ metadata:
description: >-
Guides users to create and edit standards-compliant SKILL.md skill packages. Supports the DesireCore full format (frontmatter metadata + L0/L1/L2 layered content + scripts/references/assets) and the Claude Code basic format. Use when the user requests to create a new Skill, update an existing Skill, or package experience into a reusable Skill bundle.
body: ./SKILL.md
source_hash: sha256:2e8b886dc0b77dd1
source_hash: sha256:e14a6879bb800455
translated_by: human
market:
icon: >-

View File

@@ -68,7 +68,7 @@ market:
|------|------|------|
| `market.icon` | string | 内联 SVG 图标 |
| `market.short_desc` | string | 一句话简介 |
| `market.category` | string | 分类 slug`productivity``knowledge``development` |
| `market.category` | string | 分类 slug`productivity``design``research``development` |
| `market.maintainer.name` | string | 维护者名称 |
| `market.maintainer.verified` | boolean | 是否官方认证 |
| `market.compatible_agents` | string[] | 兼容的 Agent ID |

View File

@@ -23,21 +23,59 @@ DESIRECORE_TEMPLATE = """\
---
name: {skill_name}
description: >-
[TODO: 完整描述技能用途。必须包含 "Use when" 触发提示,
帮助 AI 判断何时使用该技能。]
[TODO: Complete and informative explanation of what the skill does and when to
use it. Include "Use when" trigger hints and Chinese trigger keywords.]
version: 1.0.0
type: procedural
risk_level: low
status: enabled
tags:
- [TODO: 添加标签]
- {first_tag}
metadata:
author: user
updated_at: '{today}'
i18n:
default_locale: en-US
source_locale: zh-CN
locales:
- zh-CN
- en-US
zh-CN:
name: {skill_title}
short_desc: '[TODO: 中文一句话简介]'
description: >-
[TODO: 中文较长描述。]
body: ./SKILL.zh-CN.md
translated_by: human
en-US:
name: {skill_title}
short_desc: '[TODO: English one-line summary]'
description: >-
[TODO: Longer English description. CI can replace this when source_hash
is missing or stale.]
body: ./SKILL.md
translated_by: ai:pending
market:
icon: ''
category: productivity
channel: latest
maintainer:
name: user
verified: false
---
# {skill_title}
_Translation pending. The source body is in `SKILL.zh-CN.md`; run
`uv run scripts/i18n/translate.py {skill_path}` from the market repository to
generate this default-language body._
"""
DESIRECORE_SOURCE_BODY = """\
<!-- locale: zh-CN -->
# {skill_title}
## L0一句话摘要
[TODO: 用一句话描述这个技能做什么]
@@ -182,6 +220,8 @@ def init_skill(skill_name, path, fmt='desirecore'):
skill_content = template.format(
skill_name=skill_name,
skill_title=skill_title,
first_tag=skill_name.split('-')[0],
skill_path=f"skills/{skill_name}",
today=date.today().isoformat(),
)
@@ -193,6 +233,15 @@ def init_skill(skill_name, path, fmt='desirecore'):
print(f"❌ Error creating SKILL.md: {e}")
return None
if fmt == 'desirecore':
source_body = DESIRECORE_SOURCE_BODY.format(skill_title=skill_title)
try:
(skill_dir / 'SKILL.zh-CN.md').write_text(source_body)
print("✅ Created SKILL.zh-CN.md (source locale)")
except Exception as e:
print(f"❌ Error creating SKILL.zh-CN.md: {e}")
return None
# Create resource directories with example files
try:
scripts_dir = skill_dir / 'scripts'

View File

@@ -8,6 +8,7 @@ Also accepts Claude Code basic format (name + description only).
import sys
import re
import json
from pathlib import Path
try:
@@ -17,30 +18,36 @@ except ImportError:
sys.exit(1)
# DesireCore 已知的顶层字段集合
# 来源lib/schemas/agent/skill-frontmatter.ts 的 properties 定义
# Schema 设置了 additionalProperties: true所以未知字段只警告不报错
KNOWN_PROPERTIES = {
# 核心字段
'name', 'description', 'version', 'type', 'requires',
'risk_level', 'status', 'tags', 'metadata',
# 功能控制
'disable-model-invocation', 'disable_model_invocation',
'allowed-tools', 'user-invocable', 'argument-hint',
'model', 'context', 'agent',
# 高级字段
'error_message', 'skill_package', 'input_schema', 'output_schema',
'market', 'x_desirecore', 'json_output',
# Claude Code 兼容字段
'license', 'compatibility',
}
REPO_ROOT = Path(__file__).resolve().parents[3]
SCHEMA_PATH = REPO_ROOT / 'scripts' / 'i18n' / 'schema' / 'skill-frontmatter.schema.json'
CATEGORIES_PATH = REPO_ROOT / 'categories.json'
VALID_TYPES = {'procedural', 'conversational', 'meta'}
VALID_RISK_LEVELS = {'low', 'medium', 'high'}
VALID_STATUSES = {'enabled', 'disabled'}
def load_market_schema():
if not SCHEMA_PATH.is_file():
return {}
return json.loads(SCHEMA_PATH.read_text(encoding='utf-8'))
SCHEMA = load_market_schema()
SCHEMA_PROPERTIES = SCHEMA.get('properties', {}) if isinstance(SCHEMA, dict) else {}
KNOWN_PROPERTIES = set(SCHEMA_PROPERTIES)
REQUIRED_PROPERTIES = set(SCHEMA.get('required', [])) if isinstance(SCHEMA, dict) else set()
VALID_TYPES = set(SCHEMA_PROPERTIES.get('type', {}).get('enum', []))
VALID_RISK_LEVELS = set(SCHEMA_PROPERTIES.get('risk_level', {}).get('enum', []))
VALID_STATUSES = set(SCHEMA_PROPERTIES.get('status', {}).get('enum', []))
VALID_CONTEXTS = {'default', 'fork'}
SEMVER_RE = re.compile(r'^\d+\.\d+\.\d+$')
KEBAB_RE = re.compile(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$')
SEMVER_RE = re.compile(SCHEMA_PROPERTIES.get('version', {}).get('pattern', r'^\d+\.\d+\.\d+$'))
NAME_RE = re.compile(SCHEMA_PROPERTIES.get('name', {}).get('pattern', r'^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$'))
TAG_RE = re.compile(SCHEMA_PROPERTIES.get('tags', {}).get('items', {}).get('pattern', r'^[a-z0-9][a-z0-9-]*$'))
LOCALE_RE = re.compile(r'^[a-z]{2,3}(?:-[A-Z]{2})?$')
def load_category_ids():
if not CATEGORIES_PATH.is_file():
return set()
data = json.loads(CATEGORIES_PATH.read_text(encoding='utf-8'))
return set(data) if isinstance(data, dict) else set()
def validate_skill(skill_path):
@@ -76,8 +83,11 @@ def validate_skill(skill_path):
return False, [f"Invalid YAML: {e}"], []
# === 必填字段 ===
if 'description' not in frontmatter:
errors.append("Missing required field: 'description'")
is_basic_claude_skill = set(frontmatter).issubset({'name', 'description', 'license', 'compatibility'})
required = {'name', 'description'} if is_basic_claude_skill else REQUIRED_PROPERTIES
for field in sorted(required):
if field not in frontmatter:
errors.append(f"Missing required field: '{field}'")
# === description 质量检查 ===
description = frontmatter.get('description', '')
@@ -96,15 +106,15 @@ def validate_skill(skill_path):
n = name.strip()
if len(n) > 64:
errors.append(f"Name too long ({len(n)} chars, max 64)")
# kebab-case 检查仅当 name 是英文时
if re.match(r'^[a-z0-9-]+$', n):
if not KEBAB_RE.match(n):
warnings.append(f"Name '{n}' starts/ends with hyphen or has consecutive hyphens")
if not NAME_RE.match(n):
errors.append("Name must be lowercase ASCII kebab-case, cannot start/end with hyphen, and cannot contain consecutive hyphens")
if n != skill_path.name:
errors.append(f"Name '{n}' must equal parent directory '{skill_path.name}'")
# === version 格式检查 ===
version = frontmatter.get('version')
if version is not None and not SEMVER_RE.match(str(version)):
warnings.append(f"Version '{version}' is not valid semver (expected x.y.z)")
errors.append(f"Version '{version}' is not valid semver")
# === 枚举字段检查 ===
skill_type = frontmatter.get('type')
@@ -123,6 +133,61 @@ def validate_skill(skill_path):
if context is not None and context not in VALID_CONTEXTS:
errors.append(f"Invalid context: '{context}'. Must be one of: {', '.join(sorted(VALID_CONTEXTS))}")
tags = frontmatter.get('tags')
if tags is not None:
if not isinstance(tags, list) or not all(isinstance(x, str) for x in tags):
errors.append("tags must be a list of strings")
else:
duplicate_tags = sorted({x for x in tags if tags.count(x) > 1})
if duplicate_tags:
errors.append(f"tags must be unique; duplicates: {', '.join(duplicate_tags)}")
invalid_tags = [x for x in tags if not TAG_RE.match(x)]
if invalid_tags:
errors.append(f"Invalid tag(s): {', '.join(invalid_tags)}")
if not is_basic_claude_skill:
metadata = frontmatter.get('metadata')
if not isinstance(metadata, dict):
errors.append("metadata must be an object")
else:
i18n = metadata.get('i18n')
if not isinstance(i18n, dict):
errors.append("metadata.i18n block missing")
else:
locales = i18n.get('locales')
if not isinstance(locales, list) or not all(isinstance(x, str) and LOCALE_RE.match(x) for x in locales):
errors.append("metadata.i18n.locales must be a list of BCP-47 locale strings")
locale_set = set()
else:
locale_set = set(locales)
for key in ('default_locale', 'source_locale'):
value = i18n.get(key)
if not isinstance(value, str) or not LOCALE_RE.match(value):
errors.append(f"metadata.i18n.{key} must be a BCP-47 locale string")
elif value not in locale_set:
errors.append(f"metadata.i18n.{key} must be present in metadata.i18n.locales")
for locale in sorted(locale_set):
payload = i18n.get(locale)
if not isinstance(payload, dict):
errors.append(f"metadata.i18n.{locale} block missing")
continue
for field in ('name', 'short_desc'):
if not isinstance(payload.get(field), str) or not payload.get(field).strip():
errors.append(f"metadata.i18n.{locale}.{field} is required")
body = payload.get('body')
if body is not None:
if not isinstance(body, str) or not body.startswith('./') or not body.endswith('.md'):
errors.append(f"metadata.i18n.{locale}.body must be a relative Markdown path starting with './'")
elif not (skill_path / body.removeprefix('./')).is_file():
errors.append(f"metadata.i18n.{locale}.body points to missing file: {body}")
market = frontmatter.get('market')
if isinstance(market, dict):
category = market.get('category')
category_ids = load_category_ids()
if category_ids and category not in category_ids:
errors.append(f"market.category '{category}' is not declared in categories.json")
# === 未知字段警告(不阻断) ===
unknown = set(frontmatter.keys()) - KNOWN_PROPERTIES
if unknown: