diff --git a/pipeline_core/skill_loader.py b/pipeline_core/skill_loader.py index 6dc77d8..168ad33 100644 --- a/pipeline_core/skill_loader.py +++ b/pipeline_core/skill_loader.py @@ -189,6 +189,50 @@ def _parse_frontmatter(content: str) -> Optional[dict]: return result +# ── LLM 技能名归一化(目录层装饰容错)── + +def normalize_skill_name(name: str) -> str: + """清洗 LLM 传入技能名时携带的目录层装饰噪音: + '[产线] bid-workflow' / '**[产线] bid-workflow**: 总纲' / 'bid-workflow(总纲)' → 'bid-workflow'。 + 技能名只含字母/数字/连字符/下划线,其余皆是装饰。""" + n = (name or '').strip().replace('**', '').replace('`', '') + n = re.sub(r'^\s*(\[[^\]]*\]\s*)+', '', n) # scope 标签:[产线]/[通用]/[角色](可能多个) + n = re.split(r'[::]', n, 1)[0] # 冒号后的描述尾巴 + n = re.split(r'\s+[—–-]\s+', n, 1)[0] # 破折号描述(两侧须有空白,不伤 bid-workflow 自带连字符) + n = re.sub(r'\s*[((][^))]*[))]\s*$', '', n) # 尾部括号注释 + return n.strip() + + +def resolve_skill(merged: Dict[str, "Skill"], name: str) -> Optional["Skill"]: + """按名称在 merged 中查技能,容忍 LLM 装饰。 + 匹配顺序:精确 → 清洗后精确 → 忽略大小写 → 子串唯一命中。""" + if not name: + return None + if name in merged: + return merged[name] + cleaned = normalize_skill_name(name) + if cleaned in merged: + return merged[cleaned] + lower_map = {k.lower(): k for k in merged} + if cleaned.lower() in lower_map: + return merged[lower_map[cleaned.lower()]] + if len(cleaned) >= 4: + hits = [k for k in merged if k in cleaned or cleaned in k] + if len(hits) == 1: + return merged[hits[0]] + return None + + +def format_skill_names(merged: Dict[str, "Skill"], limit: int = 60) -> str: + """FAIL 提示用技能名列表:高优先级 scope(role/pipeline/project/org)排前,超出截断。""" + ordered = sorted(merged.values(), key=lambda s: (-SCOPE_PRIORITY.get(s.scope, 0), s.name)) + names = [s.name for s in ordered[:limit]] + text = ", ".join(names) + if len(merged) > len(names): + text += f" …(还有 {len(merged) - len(names)} 个)" + return text or "(无可用技能)" + + # ── 六级技能加载器 ── class SkillLoader: