用OpenCode折腾Skills并不是什么新鲜事但每回看到有人在群里问“Skills到底怎么从零写一个”我都有点着急——这东西门槛真没那么高。这次干脆拿一个贴近日常的场景开刀做一个完整的“网页书签”Skill让AI帮我把一串网址自动变成带标题、描述、分类的书签清单直接落盘成Markdown文件。文章会从环境准备、Skill目录结构、SKILL.md定义、辅助抓取脚本一路讲到真实测试和翻车记录。适合刚接触OpenCode、或者玩过Claude Code但还没搞懂Skills怎么迁移的朋友照着抄就行。1. 为什么用OpenCode搭一个“网页书签”Skill1.1 先搞清楚OpenCode的Skills到底是什么OpenCode是跑在终端里的AI编程智能体国内海外都有不少人在用最大的优势是模型供应商随便切、配置灵活、还支持Agent模式和MCP。而Skills是OpenCode里用来给AI注入“专业工作流”的机制本质上是一组按特定格式组织的指令文件放在项目目录或全局配置目录下。当对话内容命中Skill描述里的触发条件时模型就会自动加载这个Skill按里面写好的流程去执行而不是靠临时聊天碰运气。早期大家玩AI Agent全靠反复在prompt里强调“你要先做什么再做什么”换一个会话就全忘了。Skills解决的就是这个问题把一套可复用的工作流固化下来下次遇到相似请求模型自己会知道“该翻书了”。它跟Claude Code的skills、Codex的AGENTS.md是同一个思路但OpenCode的实现更贴近文件即配置管理起来非常直观这也是我最后主力用它的原因。1.2 为什么选“网页书签”当第一个完整案例“网页书签”这个场景看起来简单拆开之后其实覆盖了Skills开发的所有关键环节一是需要从无结构的用户输入里提取URL二是要抓取网页并解析标题、描述这涉及网络请求和HTML解析三是要做去重、分类、排序考验数据整理能力四是要把整理结果输出成结构化文件实现“从聊天到落盘”的闭环。用这个案例练手等于把Skills开发的完整套路都走了一遍。等你做完它再回去写代码审查、需求拆解、日志分析这些Skill思路会非常顺。很多教程只教你怎么放一个SKILL.md就完事了完全不提脚本和工具调用怎么配合结果用户照做之后发现模型根本干不了活。我这篇会把这些坑一步一步填平。1.3 做完之后的最终交付物长什么样动手之前先对齐目标。完成之后你会得到一个位于项目目录.opencode/下的Skill结构大概是这样的my-workspace/ ├── .opencode/ │ └── skill/ │ └── web-bookmark/ │ ├── SKILL.md │ └── scripts/ │ └── fetch_url.py └── bookmarks.md测试生成的输出文件你在OpenCode里给出一串网址说一句“把我这些链接整理成网页书签”AI就会按顺序抓取、提取、分类、去重最后交给你一份带序号、标题、链接、描述和分类的Markdown表格并自动保存为bookmarks.md。如果某个网址抓不下来它会明确告诉你原因不会假装成功。2. 动手前的准备版本、目录和Skill定义规则2.1 确认你的OpenCode版本和Skills目录我用的OpenCode版本已经进入v2系列Skills是v2重点推的能力。不同小版本对目录的命名可能不太一样有的版本生成的是.opencode/skill/有的版本是.opencode/skills/你装好之后先看一眼项目里自动生成的目录结构以实际为准。我下面统一按.opencode/skill/写如果你本地是复数形式把路径改一下就行原理完全一致。全局目录一般在~/.config/opencode/下放到全局的Skill所有项目都能用放到项目.opencode/下的则跟随项目走。我的建议是先放项目里调试跑通了再挪到全局共享。检查版本也简单终端执行opencode --version版本太老的话直接更新安装别在旧版上死磕Skills这种新特性需要新客户端支持。2.2 SKILL.md的frontmatter到底该怎么填每个Skill的核心是SKILL.md这个文件开头有一段YAML格式的frontmatter用来声明Skill的元信息最关键的两个字段是name和description。description尤其重要它是模型判断“什么时候该用这个Skill”的依据本质上就是一段触发条件描述。--- name: web-bookmark description: 当用户需要整理网页链接、收藏网址、生成书签列表、提取网页标题和描述、对URL列表进行归类去重时使用。如果用户贴出一串网址并说“整理一下”“做成书签”“保存链接”优先调用本Skill。 ---描述里要尽可能覆盖用户可能的口语表达整理链接、收藏网址、书签、网址列表、帮我存一下……关键词多了触发率才会高。如果description写得太窄比如只写“生成书签”用户说“帮我存一下这些网站”时模型就反应不过来。2.3 一个Skill至少要包含哪些文件一个完整的Skill不只是一个Markdown文件。职责分离很重要SKILL.md只负责告诉模型“流程是什么、按什么规则处理”而真正需要稳定执行的抓取、解析动作应该放到独立的脚本里让模型去调用。这样有两个好处一是脚本可以反复测试不依赖模型prompt的稳定性二是不同模型能力有差异把脏活累活留给代码输出质量会稳定得多。所以我的模板是“SKILL.md 可执行脚本”两层结构。SKILL.md里写清步骤和判断规则脚本负责网络请求和HTML解析。如果你还想让Skill更专业可以在目录里再放references/放参考文档、examples/放输入输出示例但“网页书签”这个规模一个脚本足够。3. 核心实现网页书签Skill的完整拆解与落地3.1 第一步建立目录和SKILL.md骨架先在你的工作目录里建好Skill的文件夹mkdir -p .opencode/skill/web-bookmark/scripts然后创建SKILL.md把frontmatter写好。正文部分我用一个相对完整的流程来描述包括从输入中识别URL、调用脚本抓取、按规则分类输出。给模型看的指令要像给实习生写SOP一样——步骤分明、标准明确、异常处理写在前面# Web Bookmark Skill 把用户提供的网址列表整理成结构化书签清单。 ## 工作流程 1. 收集URL - 从用户消息中提取所有 http:// 或 https:// 开头的链接。 - 如果链接后面紧跟一段文字说明把文字作为该书签的备注。 2. 抓取网页信息 - 对每个URL执行python scripts/fetch_url.py url - 脚本会返回JSON{url: ..., title: ..., desc: ..., domain: ...} - 抓取失败的URL单独记录标注失败原因不要中断整个流程。 3. 清理与去重 - 去掉URL末尾的跟踪参数utm_*、spm、from_source 等。 - 去重规则URL完全相同只保留最新一条域名相同且标题相同视为重复。 4. 自动分类 - 根据域名和标题关键词判断分类 - github、stackoverflow、mdn、developer、docs技术 - dribbble、behance、figma、design设计 - notion、trello、slack、飞书、teambition工具 - news、blog、medium、36kr、infoq资讯 - 判断不了的就归为“其他”不要瞎猜。 - 每个书签只能有一个分类。 5. 输出格式 - 按分类分组每组内保持原始顺序。 - 输出Markdown表格列序号、名称、URL、描述、分类。 - 最后询问用户是否保存为 bookmarks.md默认保存到当前工作目录。这里的“步骤分明”指的是每一步都有明确的输入输出“异常处理写在前面”指的是第2步先声明抓取失败的URL不能中断流程。这样模型在执行时才不会遇到一个404就罢工。3.2 第二步给Skill配一个能干活的抓取脚本SKILL.md再好模型伸手去抓网页还是不够可靠尤其遇到需要解析HTML的场景模型直接总结反而容易瞎编。我写了一个Python脚本fetch_url.py用requests抓页面用BeautifulSoup解析标题和meta描述。脚本很小但每行都有讲究import sys import json from urllib.parse import urlparse import requests from bs4 import BeautifulSoup url sys.argv[1] headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 } try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 优先用响应头里的charset拿不到就自动探测 if not resp.encoding or resp.encoding.lower() iso-8859-1: resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) title soup.title.string.strip() if soup.title and soup.title.string else url if len(title) 80: title title[:80] ... desc meta soup.find(meta, attrs{name: description}) if meta and meta.get(content): desc meta[content].strip()[:200] print(json.dumps({ url: url, title: title, desc: desc, domain: urlparse(url).netloc }, ensure_asciiFalse)) except requests.exceptions.Timeout: print(json.dumps({url: url, title: , desc: 抓取超时, domain: }, ensure_asciiFalse)) except requests.exceptions.HTTPError as e: print(json.dumps({url: url, title: , desc: fHTTP错误: {e.response.status_code}, domain: }, ensure_asciiFalse)) except Exception as e: print(json.dumps({url: url, title: , desc: f抓取失败: {str(e)[:100]}, domain: }, ensure_asciiFalse))注意编码处理。国内好多站点返回的响应头没声明charsetrequests默认会按ISO-8859-1解码结果中文全乱码。我在脚本里做了判断如果响应头没有明确编码就用apparent_encoding自动探测。这个坑我至少见了三回每次都有人问为什么抓到的title是乱码问题几乎都出在这一行。还有User-Agent。很多网站对裸的python-requests请求直接返回403把UA改成浏览器标准值之后大部分页面就能正常访问了。如果你抓的网站反爬更凶可以在SKILL.md里加一条重试时把scripts/fetch_url.py的请求头替换成自己的Cookie但这属于进阶玩法入门阶段不用管。3.3 第三步让模型在OpenCode里跑起来并完成归档Skill的脚本写得再好如果模型不知道该在什么时候调用它等于白搭。所以SKILL.md里第2步写得很死对每个URL执行python scripts/fetch_url.py url。这里的执行路径是相对于SKILL.md所在目录的OpenCode在执行Skill时会把工作目录切到Skill目录下所以写相对路径是安全的。实测下来OpenCode的模型对“先调用脚本拿到结果再基于结果总结”这种模式执行得很稳定。你只要在SKILL.md里把顺序写清楚模型基本不会跳过脚本直接编内容。如果你用的是带工具调用能力的模型它甚至会在脚本抓取失败后顺手访问一遍、尝试补救体验比想象中好。归档环节讲究不多但有一个小细节值得提生成bookmarks.md之前我会让模型先输出表格给用户确认确认后再写文件。不然用户其实只想要一份临时清单结果工作区里平白多了个文件还得手动删。把“是否保存”作为一个可选项反而更符合真实使用习惯。3.4 第四步接上OpenCode并做端到端测试目录建好、SKILL.md写完后重启OpenCode让技能加载。你可以直接在会话里问一句“你现在有哪些Skills”如果看到web-bookmark说明已经被识别了。测试我建议分三轮第一轮给3个常见网站验证基本流程第二轮给一个已经失效的链接验证异常处理第三轮给10个以上链接验证去重和分类效果。下面是我实际测试的一组输入帮我整理成网页书签 https://github.com/opencode-ai/opencode https://developer.mozilla.org/zh-CN/docs/Web/JavaScript https://news.ycombinator.com/ https://github.com/opencode-ai/opencode https://dribbble.com/第一轮实测输出节选序号名称URL描述分类1opencode-ai/opencodehttps://github.com/opencode-ai/opencodeOpenCode源码仓库技术2MDN Web Docshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScriptJavaScript参考文档技术3Hacker Newshttps://news.ycombinator.com/科技资讯社区资讯4Dribbblehttps://dribbble.com/设计作品分享平台设计同一个GitHub链接出现了两次去重规则生效只保留了一条很符合预期。我把分类表设计成关键词匹配为主模型自己判断为辅这样分类结果不会天马行空。4. 测试结果、效果边界和调优方向4.1 对“网页书签”效果边界的实测观察说实话我原以为这个Skill简单到不存在什么效果差异测完才发现边界情况真不少。先说做得好的对常规资讯站、文档站、开源仓库title和description抓得很准分类也基本正确最终落盘的Markdown文件格式干净可以直接导入浏览器书签或稍作转换丢到Notion里。做得不好的地方有两个一是单页应用网站比如很多Next.js、Vue写的站点HTML里根本没有完整description返回的就只有一个空壳标题二是隐私模式拦得严的网站比如有些登录后才能看的社区贴脚本拿到的是登录墙页面提取出来的description全是“登录后查看”。这种情况Skill本身处理不了需要在SKILL.md里告诉模型如果desc为空就根据URL和title写一条简短说明并标记为“未获取到描述”而不是硬编一段。4.2 调优方向从能用变成好用“能用”和“好用”之间的差距往往就差在几个细节上。第一个值得调的方向是分类规则。关键词表从个位数扩到两位数之后分类准确率会有一次明显跃升但扩到三四十个关键词之后收益就开始递减了过度细化会引入误判比如把“medium”误归为设计类网站。第二个方向是让Skill自动处理“书签名前缀”。很多网页的title带着站点名后缀比如“首页 - 知乎”存到书签里看着很冗余。可以在SKILL.md里加一条规则如果title以“站点名 - ”或“站点名 | ”结尾去掉前缀只保留页面名。这个小规则能明显提升输出观感。第三个方向是输出格式自适应。Markdown表格适合落盘但如果你希望导入浏览器浏览器书签用的是HTML或JSON格式表格就没用了。可以在SKILL.md里加一个“输出格式”参数用户要导入Chrome就给HTML要存到Obsidian就给Markdown灵活很多。4.3 性能与成本考量“网页书签”这个Skill实测下来单个URL从抓取到输出大约消耗几百token10个URL全流程跑完也没有触发明显的上下文暴涨因为每次脚本调用只把JSON结果返回给模型HTML正文不进入上下文。这一点是脚本化方案带来的最大隐性收益没让模型直接读页面源码否则几个页面下来上下文就爆了。成本上如果你用OpenCode的免费档模型一次抓取10个链接完全在可接受范围内。但如果你用付费模型建议在SKILL.md里加一句“批量URL超过15个时先让用户确认是否继续避免一次性拉太多页面”。这个习惯能帮你省不少token尤其当你把Skill分享给团队用的时候。5. 常见问题与实战避坑实录5.1 Skill不自动触发先检查这三处不少人照着教程做完发现把网址丢给OpenCode它只是普通地回复压根没调用Skill。我排查下来九成是三个原因一是description写得太窄用户的说法没命中关键词比如用户说“保存网页”你只写了“书签”二是SKILL.md放在磁盘上了但OpenCode没重启技能列表还是旧的三是模型太老或配置的模型不支持自动加载Skill手动确认一下当前模型的工具调用能力。还有一个隐蔽的坑如果你同时开了多个Skilldescription之间的关键词发生重叠模型有可能加载错。比如“网页书签”和“网页内容总结”都写了“网页”两个字用户说“帮我总结这个网页”两个都可能被触发。解决办法是让每个Skill的description更聚焦各自圈定自己的核心动作减少交叉。5.2 碰到HTTP 403/404加UA还不够怎么办增加浏览器UA是最常见的解法但遇到Cloudflare这类防护时还不够。其次能试的是把timeout10改成timeout15多给一点缓冲还有一部分网站会对陌生IP限流请求间隔加长就行在脚本里做循环请求时用time.sleep(1)控制频率。如果目标站点是明确的反爬大户比如某些电商平台网页书签Skill就不适合硬刚在SKILL.md里让模型直接跳过这类站点并提示用户“该站点可能限制自动访问”就行没必要为了一个书签把Skill做成爬虫工具。5.3 遇到免费模型额度报错先说结论OpenCode某些内置免费档模型在非OpenCode客户端内直接调用API时会看到类似 “error from provider (console): opencodes free tier can only be used from within opencode” 的提示。这句话本身已经把答案写在里面了免费档只能在OpenCode客户端内部用不能绕过客户端拿接口地址出去调。这不是你配置错了是服务端限制。解决办法很直接在OpenCode里用它跑Skills或者配置你自己的模型供应商。别琢磨绕过去没必要也不合规。5.4 其他值得记录的零零碎碎脚本输出JSON里如果带了控制台彩印模型解析会失败所以脚本里不要用print打日志只print最终JSON。如果抓取结果的title是空的模型还坚持输出“无标题”三个字体验很差。在SKILL.md里加一条title为空时直接用URL最后一段路径作为名称这样输出至少是能看的。一次给几十个链接时模型可能因为上下文太长开始丢三落四表现就是对某几个URL不抓取直接跳过。解决方式是让模型分批处理先抓前5个再抓后5个。根据我个人经验这套“描述触发脚本干活”的Skills写法是我玩OpenCode以来性价比最高的组合。清理搜索记录、批量拉取文档内容然后生成摘要、把聊天记录归档成结构化笔记都是同一套模板套出来的。网页书签只是一个起点学会了这个结构后面再想做别的Skill基本就是改SKILL.md流程和换脚本的事了。如果你在复现过程中卡在某个环节按上面“常见问题”的排查顺序走一遍大概率能自己解决。