docs: opencode skill 处理流程笔记

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
clz
2026-06-15 15:58:09 +08:00
commit 01d4fab84f
5 changed files with 412 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"vimMode": true
}
+1
View File
@@ -0,0 +1 @@
{}
+31
View File
@@ -0,0 +1,31 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
}
+173
View File
@@ -0,0 +1,173 @@
{
"main": {
"id": "ceb42b37108b79d1",
"type": "split",
"children": [
{
"id": "a70f533ca66cb134",
"type": "tabs",
"children": [
{
"id": "4faabce30e829d6d",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "skill-handling.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "skill-handling"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "0da37654e6246f0c",
"type": "split",
"children": [
{
"id": "292f54208e302576",
"type": "tabs",
"children": [
{
"id": "ec7e8197eafd1e19",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "文件列表"
}
},
{
"id": "6addc20c98ed9edc",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "搜索"
}
},
{
"id": "cc6a05353fa1d34a",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "书签"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "708100fc1b229aed",
"type": "split",
"children": [
{
"id": "8f5239695db871df",
"type": "tabs",
"children": [
{
"id": "cecc124c3d3c8223",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "skill-handling.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "skill-handling 的反向链接列表"
}
},
{
"id": "5d56891897a5ab6f",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "skill-handling.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "skill-handling 的出链列表"
}
},
{
"id": "152c0fe5a7948eff",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "标签"
}
},
{
"id": "b8702c3c3a4a60e6",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "skill-handling.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "skill-handling 的大纲"
}
}
],
"currentTab": 3
}
],
"direction": "horizontal",
"width": 300
},
"left-ribbon": {
"hiddenItems": {
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
"daily-notes:打开/创建今天的日记": false,
"templates:插入模板": false,
"command-palette:打开命令面板": false
}
},
"active": "b8702c3c3a4a60e6",
"lastOpenFiles": []
}
+204
View File
@@ -0,0 +1,204 @@
# Skill 处理流程
仓库里有两套并存实现:
- **V1**`packages/opencode/src/skill/``index.ts``discovery.ts`+ `packages/opencode/src/tool/skill.ts`
- **V2**`packages/core/src/skill.ts``packages/core/src/skill/discovery.ts``packages/core/src/skill/guidance.ts` + `packages/core/src/tool/skill.ts`
V2 用 Effect Service + 注入 SystemContext,是后续主线。整体走"**发现 → 注册到 System Context → 通过 `skill` 工具按需加载**"三段式。
## 1. 数据模型
`Info`(统一描述):
- `name` / `description` / `location``SKILL.md` 绝对路径)/ `content`markdown 正文,frontmatter 已剥离)
V2 额外抽象 `Source` 联合:`directory` / `url` / `embedded`,通过 `Source.equals` / `Source.key` 做去重(`packages/core/src/skill.ts:14-46`)。
## 2. 发现(Discovery
V1 入口 `discoverSkills``packages/opencode/src/skill/index.ts:173`),扫盘顺序:
1. **全局外部目录**`~/.claude/skills/**/SKILL.md``~/.agents/skills/**/SKILL.md`
-`--disable-external-skills``--disable-claude-code-skills` 控制
2. **项目向上递归**:从当前目录走到 `worktree` 根,查相同的 `.claude` / `.agents`
3. **opencode 自身配置目录**里的 `{skill,skills}/**/SKILL.md`
4. **`opencode.json``skills.paths`**:支持 `~/`、相对路径
5. **`skills.urls`**:交给 `Discovery.pull(url)` 把远程 `index.json` 描述的 skill 包下载到 `Global.Path.cache/skills/<name>/`
### 远程拉取的安全校验(V2`packages/core/src/skill/discovery.ts`
- `isSafeSegment` / `isSafeRelativePath` 防路径穿越
- 每个文件落地前用 `FSUtil.contains(root, dest)` 二次校验必须在该 skill 根目录里
- URL origin 必须与源一致
- 缓存目录用 `Bun.hash(base).toString(16)` 命名,已存在则跳过
## 3. 加载与去重
`add()``ConfigMarkdown.parse` 解析 frontmatter
- V1 要求 `name: string`
- V2 允许从文件名/目录推断 `name``*.md` 顶层文件用 `basename`,子目录里只接受 `SKILL.md`
行为:
- 同名 skill 输出 `Effect.logWarning("duplicate skill name", ...)`**后扫到的覆盖先扫到的**
- 解析失败通过 `EventV2Bridge``Session.Event.Error` 让 UI 看见,但**不阻塞启动**
- **内置 skill `customize-opencode`** 在磁盘扫描之前先注册(`skill/index.ts:278-283`),磁盘同名 skill 可以覆盖它——目的是防止模型瞎写 `opencode.json` 触发硬失败
V2 `list``skill.ts:140`)维护 `Map<Source.key, Info[]>` 内存缓存;目前没有 fs watcher,源码里 `QUESTION(Dax)` 标注待办。
## 4. 实例状态
V1 `Skill.layer``discovered``state` 都包进 `InstanceState.make``skill/index.ts:259-287`):
> **每个工作目录(`ctx.directory` + `ctx.worktree`)有自己的 skill 集合**
符合 `packages/opencode/AGENTS.md` 里 "两个并存目录不能共享一个 service" 规则。所有读取(`get` / `require` / `all` / `available`)都走 `InstanceState.get(state)`
## 5. 权限过滤
`available(agent)``Permission.evaluate("skill", skill.name, agent.permission)` 过滤,只保留 `action !== "deny"`
V2 `SkillV2.available``skill.ts:57`)逻辑相同,是 Agent + Permission 系统的 hook 点。
## 6. 注入 System ContextV2 `SkillGuidance`
`packages/core/src/skill/guidance.ts` 是把 skill 接入对话上下文的关键:
- 注册为 `SystemContext.Source``key = "core/skill-guidance"`
- `load` 返回当前 agent 可见 skill 的 `{name, description}` 列表
- `baseline(skills)` 渲染为:
```
Skills provide specialized instructions ...
<available_skills>
<skill><name>...</name><description>...</description></skill>
...
</available_skills>
```
- **只导出 name + descriptionbody 不出现**。对应 `CONTEXT.md`
> skill bodies and locations are exposed only through the permission-checked `skill` tool
- skill 集合变化时:
- `update` 渲染 "available skills have changed. This list supersedes ..."
- `removed` 渲染 "skill guidance is no longer available"
- 这些作为 **Mid-Conversation System Message** 在下一个 Safe Provider-Turn Boundary 注入历史
- agent 切换(`AgentV2.Selection`)触发新的 Context Epochguidance 按新 agent 重新过滤
## 7. `skill` 工具:按需读取正文
模型实际拿到 skill 内容必须经过 `skill` 工具,两套实现语义一致。
### V2`packages/core/src/tool/skill.ts`
1. `skills.list()` 取当前可见集合,按 name 匹配
2. `permission.assert({ action: "skill", resources: [skill.name], save: [skill.name], ... })` — 真正的授权点;`save` 让用户可勾选 "always allow this skill"
3. 若是 `SKILL.md` 形态,glob 同目录其它文件,最多 `FILE_LIMIT = 10` 个采样
4. `toModelOutput` 打包成固定 schema 的文本:
```
<skill_content name="...">
# Skill: ...
<content>
Base directory for this skill: file://...
<skill_files>
<file>...</file>
</skill_files>
</skill_content>
```
### V1`packages/opencode/src/tool/skill.ts`
用 `ripgrep.find` 取文件清单(`pattern: "!**/SKILL.md"``limit: 10`);
`ctx.ask({ permission: "skill", patterns, always })` 弹权限;输出格式相同。
## 8. 实际下发给 LLM 的完整 prompt 形态
### V1 渲染入口
- `SystemPrompt.skills(agent)``packages/opencode/src/session/system.ts:94-106`,内部调用 `Skill.fmt(list, { verbose: true })`
- `Skill.fmt``packages/opencode/src/skill/index.ts:330-355`
- 注释(`system.ts:102-103`)明确策略:**详细描述放 system prompt,工具描述只留指针**——经过实验确认 agent 这样吸收得更好
### V1 的 skills 段(系统消息里的一段字符串)
按 `name` 升序拼接:
```
Skills provide specialized instructions and workflows for specific tasks.
Use the skill tool to load a skill when a task matches its description.
<available_skills>
<skill>
<name>customize-opencode</name>
<description>Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.</description>
<location>file:///<built-in></location>
</skill>
<skill>
<name>some-other-skill</name>
<description>...</description>
<location>file:///abs/path/to/SKILL.md</location>
</skill>
</available_skills>
```
注意点:
- 前两行**永远渲染**`system.ts:99-105` 写死);即使 0 skill,第三段会变成 `"No skills are currently available."`
- **没有 `description` 的 skill 会被 `fmt` 过滤掉**`skill/index.ts:331`),不会出现在 prompt 里——但 `Skill.require(name)` 仍能加载它们
- `<location>` 来自 `pathToFileURL(skill.location).href`;内置 skill 的 location 字段是字符串 `"<built-in>"`,被 `pathToFileURL` 转成 `file:///<built-in>`(不是真实路径)
- agent 的 permission 整体禁用 `"skill"` 时(`Permission.disabled(["skill"], agent.permission)`),整段返回 `undefined`,最终 `system` 数组里完全没有这段
- 单个 skill 被 `Permission.evaluate("skill", name, ...) === "deny"` 时从列表剔除(`skill/index.ts:314`
### 这段在最终 system 消息数组里的位置
`packages/opencode/src/session/prompt.ts:1327-1333`
```ts
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
sys.skills(agent),
sys.environment(model),
instruction.system().pipe(Effect.orDie),
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
```
下发给 provider 的 `system: string[]` 顺序:
1. **provider 基础 prompt** —— `SystemPrompt.provider(model)` 按 `model.api.id` 选出来(`system.ts:25-39`):
- `gpt-4` / `o1` / `o3` → `beast.txt`
- 其它 `gpt` → `gpt.txt``codex` 走 `codex.txt`
- `gemini-*` → `gemini.txt`
- `claude*` → `anthropic.txt`
- 名字含 `trinity` → `trinity.txt`
- 名字含 `kimi` → `kimi.txt`
- 其它 → `default.txt`
- 这一段从更早的调用点加入,不在 `prompt.ts:1333` 拼装里
2. **`env` 段**`SystemPrompt.environment``system.ts:55-92`):包含 `model.api.id` 自我说明 + `<env>` 块(工作目录、worktree、是否 git、平台、日期),以及 `<available_references>` 块(若有 reference
3. **`instructions` 段**`Instruction.system()` 聚合的全局 + 项目 `AGENTS.md`
4. **`skills` 段**:上面那一坨
5. 可选追加 `STRUCTURED_OUTPUT_SYSTEM_PROMPT``format.type === "json_schema"`
### 工具描述(`packages/opencode/src/tool/skill.txt`)只有简短指针
```
Load a specialized skill when the task at hand matches one of the skills listed in the system prompt.
Use this tool to inject the skill's instructions and resources into current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc in the same directory as the skill.
The skill name must match one of the skills listed in your system prompt.
```
### V2 渲染差异(`packages/core/src/skill/guidance.ts:16-32`
- **没有 `<location>` 字段**,输出只含 `<name>` + `<description>`
- 0 skill 且 `PermissionV2.evaluate("skill", "*") === "deny"` 时返回 `SystemContext.empty`(整段不挂载);0 skill 但未全 deny 时仍渲染开头两行 + `"No skills are currently available."`
- skill 集合变化时**不重发整段 baseline**,而是发一条 Mid-Conversation System Message
```
The available skills have changed. This list supersedes the previous available skills list.
<新的整段>
```
- skill 集合被完全移除时发:`Skill guidance is no longer available. Do not use any previously listed skill.`
## 一句话总结
发现服务扫盘 / 拉取 → `Skill` service 在 `InstanceState` 里按 location 维护 `name → Info` 表 → `SkillGuidance` 作为 SystemContext 源把"目录"(仅 name+description)放进模型可见上下文 → 模型看到任务匹配后调用 `skill` 工具 → 工具在权限通过后才把 skill 正文和同目录文件清单注入对话。