fix: 修正 SkillItem 类型为标准的 Anthropic SKILL.md 格式

将 SkillItem 从虚构的 detail/triggers/instructions/format 字段简化
为标准 SKILL.md 定义:name + description + body。

- protocol.ts: SkillItem 精简为 {name, description, body}
- SkillsView.tsx: 从 3 层改为 2 层渐进式披露
- skills.ts / skills-loader.ts: 去掉多余的映射字段
- .gitignore: 排除外部 skills/ 仓库克隆目录
This commit is contained in:
carry
2026-06-07 22:57:46 +08:00
parent e47587f492
commit b8e4961d10
5 changed files with 41 additions and 142 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules/
dist/ dist/
.DS_Store .DS_Store
*.tsbuildinfo *.tsbuildinfo
skills/
+16 -55
View File
@@ -1,27 +1,26 @@
import { useState } from 'react' import { useState } from 'react'
import type { SkillSegment } from '../../types/protocol' import type { SkillSegment } from '../../types/protocol'
import CollapsiblePanel from '../CollapsiblePanel' import CollapsiblePanel from '../CollapsiblePanel'
import { Zap, ChevronDown, ChevronRight, Lightbulb, BookOpen } from 'lucide-react' import { Zap, ChevronDown, ChevronRight } from 'lucide-react'
/** /**
* 单个 Skill 的渐进式披露组件。 * 单个 Skill 的渐进式披露组件。
* *
* Anthropic 3 层披露机制: * Anthropic SKILL.md 格式只有两个元数据字段:name + description。
* 第 1 层 — 名称 + 一行描述(始终可见) * body 是 Markdown 正文,在 skill 触发时加载到 LLM 上下文。
* 第 2 层 — 详细说明 + 触发条件(点击展开) *
* 第 3 层 — 完整指令(再次点击展开 — 触发时作为 system 消息追加到对话,而非合并进已有 System Prompt * 第 1 层 — name + description(始终可见
* 第 2 层 — body(点击展开 —— 触发 skill 时加载到上下文)
*/ */
function SkillDisclosure({ item }: { item: SkillSegment['items'][number] }) { function SkillDisclosure({ item }: { item: SkillSegment['items'][number] }) {
const [layer, setLayer] = useState<1 | 2 | 3>(1) const [expanded, setExpanded] = useState(false)
const hasBody = item.body.trim().length > 0
const hasLayer2 = !!(item.detail || (item.triggers && item.triggers.length > 0))
const hasLayer3 = !!item.instructions
return ( return (
<li className="rounded-lg border border-green-200 bg-white overflow-hidden"> <li className="rounded-lg border border-green-200 bg-white overflow-hidden">
{/* Layer 1: 始终可见 — 名称 + 一行描述 */} {/* Layer 1: 始终可见 — name + description */}
<button <button
onClick={() => setLayer(layer === 1 && hasLayer2 ? 2 : 1)} onClick={() => hasBody && setExpanded(!expanded)}
className="w-full flex items-start gap-2.5 px-3 py-2 text-left hover:bg-green-50/50 transition-colors" className="w-full flex items-start gap-2.5 px-3 py-2 text-left hover:bg-green-50/50 transition-colors"
> >
<span className="font-mono font-semibold text-green-800 shrink-0 text-xs mt-px"> <span className="font-mono font-semibold text-green-800 shrink-0 text-xs mt-px">
@@ -30,62 +29,24 @@ function SkillDisclosure({ item }: { item: SkillSegment['items'][number] }) {
<span className="text-xs text-gray-500 flex-1 leading-relaxed"> <span className="text-xs text-gray-500 flex-1 leading-relaxed">
{item.description} {item.description}
</span> </span>
{hasLayer2 && ( {hasBody && (
<span className="shrink-0 text-green-400 mt-px"> <span className="shrink-0 text-green-400 mt-px">
{layer >= 2 ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span> </span>
)} )}
</button> </button>
{/* Layer 2: 详细说明 + 触发条件 */} {/* Layer 2: 完整指令正文(body */}
{layer >= 2 && hasLayer2 && ( {expanded && hasBody && (
<div className="px-3 pb-2 border-t border-green-100 bg-green-50/30">
{item.detail && (
<div className="mt-2 flex items-start gap-1.5">
<BookOpen size={12} className="text-green-500 shrink-0 mt-0.5" />
<p className="text-xs text-gray-600 leading-relaxed">{item.detail}</p>
</div>
)}
{item.triggers && item.triggers.length > 0 && (
<div className="mt-1.5 flex items-start gap-1.5">
<Lightbulb size={12} className="text-amber-500 shrink-0 mt-0.5" />
<div className="text-xs text-gray-500">
<span className="font-medium text-amber-700"></span>
{item.triggers.map((t, i) => (
<span key={i} className="inline-block bg-amber-50 text-amber-800 rounded px-1 py-0.5 mr-1 mt-0.5 font-mono">
"{t}"
</span>
))}
</div>
</div>
)}
{/* Layer 3 切换按钮 */}
{hasLayer3 && (
<button
onClick={(e) => { e.stopPropagation(); setLayer(layer === 2 ? 3 : 2) }}
className="mt-2 flex items-center gap-1 text-[10px] text-green-600 hover:text-green-800 transition-colors"
>
{layer === 3 ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
{item.format === 'anthropic' ? '查看原始 SKILL.md' : '查看注入指令(追加到对话)'}
</button>
)}
</div>
)}
{/* Layer 3: 完整指令 */}
{layer >= 3 && item.instructions && (
<div className="px-3 pb-2 border-t border-green-200 bg-gray-900 mx-2 mb-2 rounded"> <div className="px-3 pb-2 border-t border-green-200 bg-gray-900 mx-2 mb-2 rounded">
<div className="flex items-center gap-1.5 mt-2 mb-1"> <div className="flex items-center gap-1.5 mt-2 mb-1">
<Zap size={10} className="text-green-400" /> <Zap size={10} className="text-green-400" />
<span className="text-[10px] text-green-400 font-semibold uppercase tracking-wider"> <span className="text-[10px] text-green-400 font-semibold uppercase tracking-wider">
{item.format === 'anthropic' LLM
? `SKILL.md · 触发 /${item.name} 时加载到 LLM 上下文`
: `注入指令 · 触发 /${item.name} 时追加到对话`}
</span> </span>
</div> </div>
<pre className="text-[10px] font-mono text-gray-300 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto pb-1"> <pre className="text-[10px] font-mono text-gray-300 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto pb-1">
{item.instructions} {item.body}
</pre> </pre>
</div> </div>
)} )}
+3 -24
View File
@@ -42,19 +42,15 @@ console.log(
// ============================================================ // ============================================================
/** /**
* 渐进式披露对应关系 * 渐进式披露 2 层
* L1 — name + descriptionYAML frontmatter,始终可见) * L1 — name + descriptionYAML frontmatter,始终可见)
* L2 — body 摘要 + 触发条件(点击展开) * L2 — bodySKILL.md 正文,点击展开)
* L3 — 完整 SKILL.md 内容(再次点击展开)
*/ */
export function toSkillItem(parsed: ParsedSkill): SkillItem { export function toSkillItem(parsed: ParsedSkill): SkillItem {
return { return {
name: parsed.name, name: parsed.name,
description: parsed.description, description: parsed.description,
detail: `SKILL.md · ${parsed.bodyLineCount} 行指令 · ${parsed.bodyCharCount}\n\n${truncateLines(parsed.body, 10)}`, body: parsed.body,
triggers: extractTriggerPhrases(parsed.description),
instructions: parsed.body,
format: 'anthropic',
} }
} }
@@ -69,20 +65,3 @@ export function getRealSkills(names: string[]): SkillItem[] {
export function getAllRealSkillItems(): SkillItem[] { export function getAllRealSkillItems(): SkillItem[] {
return Object.values(PARSED_SKILLS).map(toSkillItem) return Object.values(PARSED_SKILLS).map(toSkillItem)
} }
// ---- 辅助 ----
function truncateLines(text: string, maxLines: number): string {
const lines = text.split('\n')
if (lines.length <= maxLines) return text
return lines.slice(0, maxLines).join('\n') + `\n...(共 ${lines.length} 行)`
}
function extractTriggerPhrases(description: string): string[] {
const triggers: string[] = []
const matches = description.matchAll(/"([^"]+)"/g)
for (const m of matches) {
if (m[1].length < 40) triggers.push(m[1])
}
return triggers.slice(0, 8)
}
+13 -51
View File
@@ -1,25 +1,19 @@
/** /**
* Anthropic 渐进式披露 Skills — 唯一数据源 * 自定义 Skills —— 模拟 Anthropic SKILL.md 格式
* *
* 每个 Skill 包含 3 层信息: * 每个 skill 只有三个字段:name、descriptionYAML frontmatter)、
* L1 — name + description(始终可见) * bodyMarkdown 正文 —— 触发时加载到 LLM 上下文的指令)。
* L2 — detail + triggers(点击展开)
* L3 — instructions(再次点击展开,触发时注入对话)
* *
* Demo 场景通过 `getSkills(...)` 按名称选取子集,避免数据重复 * Demo C/D/E 通过 getSkills() 按名称选取子集。
*/ */
import type { SkillItem } from '../types/protocol' import type { SkillItem } from '../types/protocol'
export const ALL_SKILLS: Record<string, SkillItem> = { export const ALL_SKILLS: Record<string, SkillItem> = {
// ── 研究 & 文献 ──
'deep-research': { 'deep-research': {
name: 'deep-research', name: 'deep-research',
description: '深度研究 — 多源搜索、交叉验证、生成引用报告', description: '深度研究 — 多源搜索、交叉验证、生成引用报告',
detail: body: `你是一名深度研究助手。工作流程:
'多阶段研究技能:(1) 拆解问题为 3-5 个子问题,(2) 并行搜索学术文献、行业报告、新闻等多个来源,(3) 抓取高相关性全文,(4) 三方交叉验证——至少两个独立来源确认同一关键事实,(5) 生成结构化报告:摘要 → 分项发现 → 证据表 → 限定说明。适合需要高质量、可验证答案的场景。',
triggers: ['深入调研', '全面分析', '研究一下', '查证', '给我一个研究报告', '这个领域有哪些'],
instructions: `你是一名深度研究助手。工作流程:
1. 分拆用户问题为 3-5 个子问题 1. 分拆用户问题为 3-5 个子问题
2. 对每个子问题执行多源搜索(学术 + 行业 + 新闻) 2. 对每个子问题执行多源搜索(学术 + 行业 + 新闻)
3. 抓取高相关性页面全文 3. 抓取高相关性页面全文
@@ -28,14 +22,10 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
6. 每个声明确标注来源 URL 和可信度评级`, 6. 每个声明确标注来源 URL 和可信度评级`,
}, },
// ── 代码审查 ──
'code-review': { 'code-review': {
name: 'code-review', name: 'code-review',
description: '代码审查 — 发现正确性 bug 和简化/效率优化机会', description: '代码审查 — 发现正确性 bug 和简化/效率优化机会',
detail: body: `你是代码审查专家。审查规则:
'审查当前分支的代码变更,按两个维度分析:(1) 正确性问题——空值、边界条件、竞态、资源泄漏;(2) 质量改进——重复代码、过度复杂、性能瓶颈。按置信度分级输出,每个发现包含文件路径、行号、问题描述和修复建议。',
triggers: ['review', '审查', '帮我看看代码', '代码有什么问题', '检查一下'],
instructions: `你是代码审查专家。审查规则:
1. 首先读取 diffgit diff 1. 首先读取 diffgit diff
2. 正确性扫描:空值处理、边界条件、竞态条件、异常处理 2. 正确性扫描:空值处理、边界条件、竞态条件、异常处理
3. 质量扫描:重复代码、过长函数、深层嵌套、无用变量 3. 质量扫描:重复代码、过长函数、深层嵌套、无用变量
@@ -44,14 +34,10 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
6. 避免纯风格的评论(交给 formatter)`, 6. 避免纯风格的评论(交给 formatter)`,
}, },
// ─ 行为验证 ──
verify: { verify: {
name: 'verify', name: 'verify',
description: '行为验证 — 运行应用并观察行为来确认变更生效', description: '行为验证 — 运行应用并观察行为来确认变更生效',
detail: body: `你是验证助手。验证流程:
'启动应用、运行测试、或执行指定命令,并观察输出来验证某个变更是否按预期工作。支持多种验证策略:自动测试(优先)、端到端检查、手动观察。适合 PR 合并前的最终确认环节。',
triggers: ['验证', '测试一下', '确认', '检查是否生效', '跑一下', '运行'],
instructions: `你是验证助手。验证流程:
1. 确认待验证的变更是什么 1. 确认待验证的变更是什么
2. 选择验证策略:优先自动测试,其次手动观察 2. 选择验证策略:优先自动测试,其次手动观察
3. 运行相关测试套件 3. 运行相关测试套件
@@ -59,14 +45,10 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
5. 输出验证报告:测试结果 + 观察到的问题 + 置信度`, 5. 输出验证报告:测试结果 + 观察到的问题 + 置信度`,
}, },
// ── 代码简化 ──
simplify: { simplify: {
name: 'simplify', name: 'simplify',
description: '代码简化 — 审查代码的复用性、简洁性和效率并应用修复', description: '代码简化 — 审查代码的复用性、简洁性和效率并应用修复',
detail: body: `你是代码简化专家。简化规则:
'只关注代码质量改进(不改行为)。扫描变更文件,发现:可抽取的共享逻辑、可合并的重复代码、不必要的中间变量、可简化表达式。直接应用修复到工作树。建议 code-review 先跑完后再用此技能。',
triggers: ['简化', '重构', '精简', '优化这段代码', '能不能更简单'],
instructions: `你是代码简化专家。简化规则:
1. 只做质量改进,不改变行为 1. 只做质量改进,不改变行为
2. 发现重复代码 → 提取为函数/变量 2. 发现重复代码 → 提取为函数/变量
3. 发现过长函数 → 提取子步骤 3. 发现过长函数 → 提取子步骤
@@ -75,14 +57,10 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
6. 修改后运行全部测试确认无回归`, 6. 修改后运行全部测试确认无回归`,
}, },
// ── 定时循环 ──
loop: { loop: {
name: 'loop', name: 'loop',
description: '定时循环 — 按指定间隔重复执行一个命令或 prompt', description: '定时循环 — 按指定间隔重复执行一个命令或 prompt',
detail: body: `你是定时任务助手。循环配置:
'设置定时任务,每隔指定时间(5 分钟、30 分钟、1 小时等)自动执行。适合 CI 监控、定时检查、长期运行的自动化任务。任务在后台运行,不阻塞当前对话。',
triggers: ['定时', '每隔', '循环', '持续监控', '定期检查', '每分钟'],
instructions: `你是定时任务助手。循环配置:
1. 确认循环间隔和任务内容 1. 确认循环间隔和任务内容
2. 首次执行立即运行一次 2. 首次执行立即运行一次
3. 后续按间隔自动触发 3. 后续按间隔自动触发
@@ -91,42 +69,30 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
6. 长时间循环(>1 小时)使用持久化模式`, 6. 长时间循环(>1 小时)使用持久化模式`,
}, },
// ── 文档摘要 ──
summarize: { summarize: {
name: 'summarize', name: 'summarize',
description: '生成文档摘要 — 支持多种粒度(一句话/段落级/全文级)', description: '生成文档摘要 — 支持多种粒度(一句话/段落级/全文级)',
detail: body: `你是专业文档摘要助手。当用户请求摘要时:
'对用户提供的文档生成结构化摘要。支持三种粒度:一句话概览(≤50 字)、段落级摘要(保存关键论点)、全文级摘要(保留章节结构)。输出为 Markdown 格式。',
triggers: ['帮我总结一下', '概括这篇文章', '这个文档说了什么', '摘要'],
instructions: `你是专业文档摘要助手。当用户请求摘要时:
1. 先确认用户需要的粒度(简要/段落/全文) 1. 先确认用户需要的粒度(简要/段落/全文)
2. 提取核心论点和支撑证据 2. 提取核心论点和支撑证据
3. 使用 Markdown 层级结构输出 3. 使用 Markdown 层级结构输出
4. 在末尾标注信息来源(章节/页码)`, 4. 在末尾标注信息来源(章节/页码)`,
}, },
// ── 翻译 ──
translate: { translate: {
name: 'translate', name: 'translate',
description: '翻译文档内容 — 支持中英互译,保留原文格式', description: '翻译文档内容 — 支持中英互译,保留原文格式',
detail: body: `你是专业翻译助手。翻译规则:
'将文档内容翻译为目标语言。保留原始 Markdown 格式、代码块、表格结构。支持术语表自定义。默认输出:中文 ↔ 英文。',
triggers: ['翻译', 'translate', '翻成中文', '译成英文'],
instructions: `你是专业翻译助手。翻译规则:
1. 保留所有 Markdown 格式和代码块 1. 保留所有 Markdown 格式和代码块
2. 术语一致性——同一术语全文统一译法 2. 术语一致性——同一术语全文统一译法
3. 学术文本保留原文关键术语括号标注 3. 学术文本保留原文关键术语括号标注
4. 表格和列表结构不变`, 4. 表格和列表结构不变`,
}, },
// ── 文档问答 ──
qa: { qa: {
name: 'qa', name: 'qa',
description: '基于文档回答具体问题 — 带引用溯源', description: '基于文档回答具体问题 — 带引用溯源',
detail: body: `你基于文档回答用户问题。规则:
'基于用户提供的文档内容回答具体问题。每个回答都附带文档引用(章节/段落号),用户可以追溯来源。支持追问和澄清。如果文档中没有相关信息,明确告知。',
triggers: ['文档中', '根据文章', '论文里提到', '这段说的是'],
instructions: `你基于文档回答用户问题。规则:
1. 每个陈述必须引用文档出处 1. 每个陈述必须引用文档出处
2. 如果文档中没有相关信息,明确告知 2. 如果文档中没有相关信息,明确告知
3. 区分"文档直接陈述"和"你的推理延伸" 3. 区分"文档直接陈述"和"你的推理延伸"
@@ -134,14 +100,10 @@ export const ALL_SKILLS: Record<string, SkillItem> = {
}, },
} }
/**
* 按名称选取一个或多个 skill 定义。
* 用于构建 PromptEnvelope 中的 skills segment。
*/
export function getSkills(names: string[]): SkillItem[] { export function getSkills(names: string[]): SkillItem[] {
return names.map((n) => { return names.map((n) => {
const skill = ALL_SKILLS[n] const skill = ALL_SKILLS[n]
if (!skill) throw new Error(`Unknown skill: ${n}`) if (!skill) throw new Error(`Unknown skill: ${n}`)
return { ...skill } // shallow copy so demos don't mutate entries return { ...skill }
}) })
} }
+8 -12
View File
@@ -83,23 +83,19 @@ export interface SkillSegment {
* 第 3 层 — 完整指令(再次点击展开 —— 触发时作为一条新消息追加到对话中) * 第 3 层 — 完整指令(再次点击展开 —— 触发时作为一条新消息追加到对话中)
*/ */
/** /**
* Skill 遵循 Anthropic 渐进式披露机制: * Skill 遵循 Anthropic SKILL.md 规范。
* *
* 第 1 层 — 名称 + 一句话描述(始终可见,在 skills 面板中) * 标准 YAML frontmatter 只有两个必填字段:name + description。
* 第 2 层 — 详细说明 + 触发条件(点击展开单个 skill) * Markdown body 是指令正文,在 skill 被触发时加载到 LLM 上下文。
* 第 3 层 — 完整指令(再次点击展开 —— 触发时注入上下文的 system prompt
* *
* format 字段区分来源 * 渐进式披露 2 层
* 'custom' — 手工编写的 skill(使用 detail/triggers/instructions 自定义 * L1 — name + description(始终在上下文中,约 100 词
* 'anthropic' — 从 SKILL.md 解析(instructions 为原始 body * L2 — bodyskill 触发时加载,建议 <500 行
*/ */
export interface SkillItem { export interface SkillItem {
name: string name: string
description: string // 第 1 层:一句话描述 description: string
detail?: string // 第 2 层:详细说明(功能、输入输出、适用场景) body: string // Markdown 正文 —— 触发 skill 时加载到 LLM 上下文的指令
triggers?: string[] // 第 2 层:触发条件(用户说哪些话会触发此 skill)
instructions?: string // 第 3 层:注入 LLM 上下文的完整 system prompt
format?: 'custom' | 'anthropic' // 来源格式
} }
export interface ToolOverviewSegment { export interface ToolOverviewSegment {