mirror of
https://git.openapi.site/https://github.com/desirecore/market.git
synced 2026-07-23 05:23:40 +08:00
强化市场校验与索引元数据
合并市场索引、分类、i18n 校验、skill-creator 工具和已修改 skill 版本号更新。
This commit is contained in:
@@ -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: >-
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user