一、Skill 的数据模型

export const Info = z.object({
  name: z.string(),        
  description: z.string(), 
  location: z.string(),    
  content: z.string(),     )
})

name:skill 的唯一标识名,在frontmatter 中提取

description:描述,用于AI判断

location:SKILL.md 文件在磁盘上的绝对路径

content:指令内容,SKILL.md 正文内容

二、Skill 的发现与加载

Skill 的发现逻辑通过 state 函数懒初始化,按优先级依次扫描 4 类来源,后加载的会覆盖同名 skill(项目级覆盖全局级)

外部兼容目录

先扫全局 home 目录下的 .claude/skills/.agents/skills/


const EXTERNAL_DIRS = [".claude", ".agents"]
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {

  for (const dir of EXTERNAL_DIRS) {
    const root = path.join(Global.Path.home, dir)
    if (!(await Filesystem.isDir(root))) continue
    await scanExternal(root, "global")
  }

  for await (const root of Filesystem.up({
    targets: EXTERNAL_DIRS,
    start: Instance.directory,
    stop: Instance.worktree,
  })) {
    await scanExternal(root, "project")
  }
}

.opencode 自有目录

扫描 opencode 原生的 skill 目录,支持 skill/ 和 skills/ 两种命名。即.opencode/skill/.opencode/skills/

const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
for (const dir of await Config.directories()) {
  const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, { ... })
  for (const match of matches) await addSkill(match)
}

用户自定义路径

用户可以在 opencode.json 中声明自定义的 skill 路径(支持 ~/ 前缀和相对路径展开):

远程 URL 下载

通过 Discovery.pull(url) 从远程服务器拉取 skill:

三、Skill的注册

每发现一个 SKILL.md,都用这个addSkill函数解析并注册到内存 map 中:

注册表是一个以 name 为 key 的 Record<string, Info> 对象,存储在 state 中。

const addSkill = async (match: string) => {
  // 解析 SKILL.md(YAML frontmatter + 正文)
  const md = await ConfigMarkdown.parse(match).catch((err) => {

    Bus.publish(Session.Event.Error, { ... })
    return undefined
  })

  const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
  if (!parsed.success) return


  if (skills[parsed.data.name]) {
    log.warn("duplicate skill name", { ... })
  }

  // 注册到内存 map
  skills[parsed.data.name] = {
    name: parsed.data.name,
    description: parsed.data.description,
    location: match,          // 文件路径
    content: md.content,      // 正文内容
  }
  dirs.add(path.dirname(match))
}

四、Skill的初始化

Instance.state() 底层调用 State.create(),实现基于项目路径的单例懒加载:

// state.ts
export function create<S>(root: () => string, init: () => S, ...) {
  return () => {
    const key = root()             
    const exists = entries.get(init)
    if (exists) return exists.state 
    const state = init()           
    entries.set(init, { state, dispose })
    return state
  }
}

五、Skill写入系统提示

Skill 的执行分为两个阶段:系统提示阶段(预告)和 工具调用阶段(加载详情)。

export async function skills(agent: Agent.Info) {
  // 检查 agent 权限,若 skill 工具被完全禁用则跳过
  if (PermissionNext.disabled(["skill"], agent.permission).has("skill")) return

  const list = await Skill.available(agent)  // 按 agent 权限过滤

  return [
    "Skills provide specialized instructions and workflows for specific tasks.",
    "Use the skill tool to load a skill when a task matches its description.",
    Skill.fmt(list, { verbose: true }),  // 以 XML 格式列出所有 skill 摘要
  ].join("\n")
}

Skill.fmt(list, { verbose: true }) 生成的 XML 片段被注入系统提示,AI 可以提前感知到有哪些 skill 可用

六、Skill完整信息获取

export const SkillTool = Tool.define("skill", async (ctx) => {
  // --- 动态生成工具描述(含当前可用 skill 列表)---
  const list = await Skill.available(ctx?.agent)
  const description = list.length === 0
    ? "No skills are currently available."
    : [
        "Load a specialized skill...",
        Skill.fmt(list, { verbose: false }),  // 简洁列表(工具描述用简洁版,系统提示用详细版)
      ].join("\n")

  return {
    description,
    parameters: z.object({
      name: z.string().describe(`The name of the skill from available_skills (e.g., 'skill-a', ...)`)
    }),

    async execute(params, ctx) {
      // 1. 从注册表查找 skill
      const skill = await Skill.get(params.name)
      if (!skill) {
        const available = await Skill.all().then((x) => x.map((s) => s.name).join(", "))
        throw new Error(`Skill "${params.name}" not found. Available: ${available}`)
      }

      // 2. 权限确认(可能触发用户交互弹窗)
      await ctx.ask({
        permission: "skill",
        patterns: [params.name],
        always: [params.name],
        metadata: {},
      })

      // 3. 扫描 skill 目录的附属文件(最多 10 个,排除 SKILL.md 本身)
      const dir = path.dirname(skill.location)
      const base = pathToFileURL(dir).href
      const files = await iife(async () => {
        const arr = []
        for await (const file of Ripgrep.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort })) {
          if (file.includes("SKILL.md")) continue
          arr.push(path.resolve(dir, file))
          if (arr.length >= 10) break
        }
        return arr
      }).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))

      // 4. 拼装完整输出,注入对话上下文
      return {
        title: `Loaded skill: ${skill.name}`,
        output: [
          `<skill_content name="${skill.name}">`,
          `# Skill: ${skill.name}`,
          "",
          skill.content.trim(),         // SKILL.md 完整正文指令
          "",
          `Base directory for this skill: ${base}`,  // 附属资源的基准路径
          "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
          "Note: file list is sampled.",
          "",
          "<skill_files>",
          files,                         // 附属文件列表(供 AI 进一步读取)
          "</skill_files>",
          "</skill_content>",
        ].join("\n"),
        metadata: { name: skill.name, dir },
      }
    },
  }
})

以上代码的主要流程(execute方法):

  1. 查找 Skill:根据 LLM 传入的 name 参数,通过 Skill.get(name) 查找对应的 skill。如果找不到就抛出错误并列出所有可用的 skill 名称。

  2. 权限检查:调用 ctx.ask() 发起一个权限请求(类型为 "skill"),如果用户配置了需要确认,会弹出交互式确认。always 字段设置为 skill 名称,意味着一旦用户允许过一次,后续不再重复询问。

  3. 收集附带文件:通过 Ripgrep.files() 列出 skill 所在目录下的所有文件(排除 SKILL.md 本身),最多收集 10 个。这些文件路径会以 <file>...</file> 标签的形式包含在输出中,让 LLM 知道 skill 目录下还有哪些脚本、模板等资源可以配合使用。

  4. 构造输出:最终返回给 LLM 的是一个结构化的文本块:

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐