☰
用OpenCode打造网页书签Skill:从零到落盘的完整实战
2026/9/28 12:55:53 网站建设 项目流程

用OpenCode折腾Skills并不是什么新鲜事,但每回看到有人在群里问“Skills到底怎么从零写一个”,我都有点着急——这东西门槛真没那么高。这次干脆拿一个贴近日常的场景开刀:做一个完整的“网页书签”Skill,让AI帮我把一串网址自动变成带标题、描述、分类的书签清单,直接落盘成Markdown文件。文章会从环境准备、Skill目录结构、SKILL.md定义、辅助抓取脚本,一路讲到真实测试和翻车记录。适合刚接触OpenCode、或者玩过Claude Code但还没搞懂Skills怎么迁移的朋友,照着抄就行。

1. 为什么用OpenCode搭一个“网页书签”Skill

1.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, headers=headers, timeout=10) 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_ascii=False)) except requests.exceptions.Timeout: print(json.dumps({"url": url, "title": "", "desc": "抓取超时", "domain": ""}, ensure_ascii=False)) except requests.exceptions.HTTPError as e: print(json.dumps({"url": url, "title": "", "desc": f"HTTP错误: {e.response.status_code}", "domain": ""}, ensure_ascii=False)) except Exception as e: print(json.dumps({"url": url, "title": "", "desc": f"抓取失败: {str(e)[:100]}", "domain": ""}, ensure_ascii=False))

注意编码处理。国内好多站点返回的响应头没声明charset,requests默认会按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从抓取到输出大约消耗几百token,10个URL全流程跑完也没有触发明显的上下文暴涨,因为每次脚本调用只把JSON结果返回给模型,HTML正文不进入上下文。这一点是脚本化方案带来的最大隐性收益:没让模型直接读页面源码,否则几个页面下来上下文就爆了。

成本上,如果你用OpenCode的免费档模型,一次抓取10个链接完全在可接受范围内。但如果你用付费模型,建议在SKILL.md里加一句“批量URL超过15个时,先让用户确认是否继续,避免一次性拉太多页面”。这个习惯能帮你省不少token,尤其当你把Skill分享给团队用的时候。

5. 常见问题与实战避坑实录

5.1 Skill不自动触发?先检查这三处

不少人照着教程做完,发现把网址丢给OpenCode,它只是普通地回复,压根没调用Skill。我排查下来,九成是三个原因:一是description写得太窄,用户的说法没命中关键词,比如用户说“保存网页”,你只写了“书签”;二是SKILL.md放在磁盘上了,但OpenCode没重启,技能列表还是旧的;三是模型太老或配置的模型不支持自动加载Skill,手动确认一下当前模型的工具调用能力。

还有一个隐蔽的坑:如果你同时开了多个Skill,description之间的关键词发生重叠,模型有可能加载错。比如“网页书签”和“网页内容总结”都写了“网页”两个字,用户说“帮我总结这个网页”,两个都可能被触发。解决办法是让每个Skill的description更聚焦,各自圈定自己的核心动作,减少交叉。

5.2 碰到HTTP 403/404,加UA还不够怎么办

增加浏览器UA是最常见的解法,但遇到Cloudflare这类防护时还不够。其次能试的是把timeout=10改成timeout=15,多给一点缓冲;还有一部分网站会对陌生IP限流,请求间隔加长就行,在脚本里做循环请求时用time.sleep(1)控制频率。

如果目标站点是明确的反爬大户,比如某些电商平台,网页书签Skill就不适合硬刚,在SKILL.md里让模型直接跳过这类站点并提示用户“该站点可能限制自动访问”就行,没必要为了一个书签把Skill做成爬虫工具。

5.3 遇到免费模型额度报错,先说结论

OpenCode某些内置免费档模型在非OpenCode客户端内直接调用API时,会看到类似 “error from provider (console): opencode's 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流程和换脚本的事了。如果你在复现过程中卡在某个环节,按上面“常见问题”的排查顺序走一遍,大概率能自己解决。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询