修复 _parse_frontmatter / _scan_skills 的若干边界判断问题
背景
learn-cc/skills/skills.py 中的 _parse_frontmatter 与 _scan_skills 负责扫描 skills/ 目录、解析每个 SKILL.md 的 YAML frontmatter,并把结果注册进 SKILL_REGISTRY。原实现在常规模板下能正常工作,但对几类边界输入不够鲁棒,可能导致误解析、抛异常或注册表被污染。
改动一览
- text.startswith("---") → text.startswith("---\n")
问题:原判断只检查前 3 个字符,会把以下输入误识别为带 frontmatter:
--- 一些文字(破折号开头的普通段落)
----分隔线 / ---some-flag
正文内出现 Markdown 水平线 --- 的情况
一旦进入解析分支,split("---", 2) 可能把正文里的 --- 当成 frontmatter 边界拆开,把一段正文误送进 yaml.safe_load。
修复:要求开头必须是 --- + 换行符,符合 YAML frontmatter 规范
- yaml.safe_load(...) 后增加 isinstance(meta, dict) 检查
问题:yaml.safe_load 不只返回 dict——如果 frontmatter 写成纯字符串、列表或数字(虽然不规范,但用户可能写错),会返回非 dict 对象。后续 meta.get("name") 会直接抛 AttributeError,导致整个 skill 扫描中断。
修复:解析后兜底为 {},让后续逻辑走 fallback 分支即可。
- meta.get("name", d.name) → meta.get("name") or d.name
问题:dict.get(k, default) 只在 key 不存在时返回 default。如果 YAML 里写了 name:(值为空 → None),get 会返回 None,导致 SKILL_REGISTRY[None] = {...},后续 load_skill 全部失效。
修复:用 or 把 None / 空串也视作"未提供",统一退到目录名兜底。description 字段同理。
- description 兜底源由 raw 改为 body
问题:原写法 raw.split("\n")[0] 取的是整篇 SKILL.md 的第一行。当 frontmatter 存在时,第一行就是 ---,lstrip("#").strip() 之后变成空串——description 永远是空的。
修复:从 body(已去除 frontmatter 的正文)取首行,并改用 split("\n", 1)[0] 避免把整篇文档全切一遍(小幅性能优化)。
影响范围
对标准 SKILL.md(frontmatter 写全):行为完全一致,没有 regression。
对异常 / 不规范输入:从"抛异常 / 静默错误"改为"安全降级 + 目录名兜底"。
测试
可用以下几类输入回归:
标准模板(参考 roll-dice 的 SKILL.md)— 应正常解析 name / description / content。
frontmatter 缺失字段(如只有 name: 没有值)— 应 fallback 到目录名 / body 首行。
正文中包含 --- Markdown 水平线 — 不应被误识别为 frontmatter 边界。
frontmatter YAML 写成非字典(如 --- \nhello\n---)— 不应抛 AttributeError。
def _parse_frontmatter(text: str) -> tuple[dict, str]:
""" 解析文本中的 YAML frontmatter """
# new 在此处加入换行符, 标准SKILL.md都是---单独成行
if not text.startswith("---\n"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
try:
# 得到skill的标题
meta = yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
meta = {}
# 加一道类型检查,防止yaml解析失败
if not isinstance(meta, dict):
meta = {}
return meta, parts[2].strip()
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
# 查找skill目录
if not d.is_dir():
continue
manifest = d / "SKILL.md"
# In an standard SIILL.md template, we can find the basic structure of this
# ---
# name: roll-dice
# description: Roll dice using a random number generator. Use when asked to roll a die (d6, d20, etc.), roll dice, or generate a random dice roll.
# ---
# To roll a die, use the following command that generates a random number from 1
# to the given number of sides:
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
# 首先处理meta, 这里使用了or兜底, 因为有些skill可能name没写
name = meta.get("name") or d.name
# 直接获取description的value数据, 如果前者不存在, 用正文首行兜底
desc = meta.get("description") or body.split("\n", 1)[0].lstrip("#").strip()
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": body}
_scan_skills()
修复 _parse_frontmatter / _scan_skills 的若干边界判断问题
背景
learn-cc/skills/skills.py 中的 _parse_frontmatter 与 _scan_skills 负责扫描 skills/ 目录、解析每个 SKILL.md 的 YAML frontmatter,并把结果注册进 SKILL_REGISTRY。原实现在常规模板下能正常工作,但对几类边界输入不够鲁棒,可能导致误解析、抛异常或注册表被污染。
改动一览
问题:原判断只检查前 3 个字符,会把以下输入误识别为带 frontmatter:
--- 一些文字(破折号开头的普通段落)
----分隔线 / ---some-flag
正文内出现 Markdown 水平线 --- 的情况
一旦进入解析分支,split("---", 2) 可能把正文里的 --- 当成 frontmatter 边界拆开,把一段正文误送进 yaml.safe_load。
修复:要求开头必须是 --- + 换行符,符合 YAML frontmatter 规范
问题:yaml.safe_load 不只返回 dict——如果 frontmatter 写成纯字符串、列表或数字(虽然不规范,但用户可能写错),会返回非 dict 对象。后续 meta.get("name") 会直接抛 AttributeError,导致整个 skill 扫描中断。
修复:解析后兜底为 {},让后续逻辑走 fallback 分支即可。
问题:dict.get(k, default) 只在 key 不存在时返回 default。如果 YAML 里写了 name:(值为空 → None),get 会返回 None,导致 SKILL_REGISTRY[None] = {...},后续 load_skill 全部失效。
修复:用 or 把 None / 空串也视作"未提供",统一退到目录名兜底。description 字段同理。
问题:原写法 raw.split("\n")[0] 取的是整篇 SKILL.md 的第一行。当 frontmatter 存在时,第一行就是 ---,lstrip("#").strip() 之后变成空串——description 永远是空的。
修复:从 body(已去除 frontmatter 的正文)取首行,并改用 split("\n", 1)[0] 避免把整篇文档全切一遍(小幅性能优化)。
影响范围
对标准 SKILL.md(frontmatter 写全):行为完全一致,没有 regression。
对异常 / 不规范输入:从"抛异常 / 静默错误"改为"安全降级 + 目录名兜底"。
测试
可用以下几类输入回归:
标准模板(参考 roll-dice 的 SKILL.md)— 应正常解析 name / description / content。
frontmatter 缺失字段(如只有 name: 没有值)— 应 fallback 到目录名 / body 首行。
正文中包含 --- Markdown 水平线 — 不应被误识别为 frontmatter 边界。
frontmatter YAML 写成非字典(如 --- \nhello\n---)— 不应抛 AttributeError。