练习开发Skill——网页内容抓取Skill(website-content-fetch)

练习开发Skill——网页内容抓取Skill(website-content-fetch)
现在使用AI帮我们找一些资料帮我们分析问题的场景多的数不胜数,但是在AI找资料的过程中,我们对AI抓取的内容是不知道,也不可以明确指定范围的,主要是靠模型本身能力去收集,当然也可以增加提示词,加以控制。

当然目前解决方案也有很多:

  • 增加更详细的提示词,描述更细致,控制更精细,过程更明确
  • 同时也有Tavily Search、SearXNG等搜索智能体,可以更好指定搜索参数,如何处理搜索结果等
  • 引用Skills、MCP等丰富大模型能力

了解到这些的时候,想着练习写一个Skills,实现网页内容抓取(其实很多东西都已经实现了,本文就是学习和分享),也了解一下Skills的开发

Skills的项目结构

skill-name/ ├── SKILL.md (唯一必需) │ ├── YAML 格式 (name, description 必须) │ └── Markdown instructions (介绍使用Markdown) └── Bundled Resources (可选的其他内容,和SKILL.md同级) ├── scripts/ - 存放可执行脚本(例如 Python 等) ├── references/ - 存放文档、API说明、领域知识 ├── examples/ - 存放示例文件 ├── evals/ - 存放测试说明 └── assets/ - 准备模板、图标、样板代码。确保格式正确(PPT、Word、图片等) 

SKILL.md元数据介绍

元数据字段:

字段必填说明
nameSkill 显示名称,默认使用目录名,仅支持小写字母、数字和短横线(最长 64 字符)
description技能用途及使用场景,Claude 根据它判断是否自动应用
argument-hint自动补全时显示的参数提示,如 [issue-number]、[filename] [format]
disable-model-invocation设为 true 禁止 Claude 自动触发,仅能手动 /name 调用(默认 false)
user-invocable设为 false 从 / 菜单隐藏,作为后台增强能力使用(默认 true)
allowed-toolsSkill 激活时 Claude 可无授权使用的工具
modelSkill 激活时使用的模型
context设为 fork 时在子代理上下文中运行
agent子代理类型(配合 context: fork 使用)
hooks技能生命周期钩子配置

scripts

Skills采用Prompt + Scripts架构,Scripts必须绑定特定运行时环境

  • Skills的实现多采用Python脚本,也是大模型运行的主要环境
  • Node.js Skills:需配置node_modules及package.json
  • Bash Scripts:仅需基础Shell环境(但可能依赖系统工具包)

开发案例

Python还不太熟悉,用Node写了一个

主要实现的是:获取网页中所有的文本内容,如果有可以识别的媒体文件,将媒体资源的URL获取下来

整体项目目录结构,skillswebsite-content-fetch是本次的skill主目录,我们可以在一个项目中开发多个skill统一放在skills中,在一起维护也可以随时使用和优化其他skill

请添加图片描述

SKILL.md

--- name: website-content-fetch description: Fetch and extract content from websites. Use this skill whenever the user mentions fetching website content, extracting text from web pages, or needing to get content from a URL, even if they don't explicitly ask for a 'website content fetch' skill. --- 

package.json

{"name":"website-content-fetch","version":"1.0.0","description":"fetching website content","main":"scripts/fetch-content.js","scripts":{"test":"node scripts/fetch-content.js"},"keywords":["openclaw","skill","website","content","fetch"],"author":"","license":"MIT","dependencies":{"axios":"^1.6.2","cheerio":"^1.0.0-rc.12"}}

scripts

fetch-content.js

const axios =require("axios");const cheerio =require("cheerio");const path =require("path");const fs =require("fs");/** * Fetch website content * @param {string} url - The URL to fetch * @param {object} options - Optional parameters * @param {string} options.saveDir - Directory to save media files (optional) * @returns {Promise<object>} - The fetched content and metadata */asyncfunctionfetchWebsiteContent(url, options ={}){try{const response =await axios.get(url);const $ = cheerio.load(response.data);// Extract text contentlet content =$("body").text().trim();// Clean up content content = content.replace(/\s+/g," ");// Extract media resourcesconst media ={images:[],videos:[],audios:[],};// Extract images$("img").each((i, elem)=>{const src =$(elem).attr("src");const alt =$(elem).attr("alt")||"";if(src){const absoluteUrl =newURL(src, url).href; media.images.push({url: absoluteUrl,alt: alt,});}});// Extract videos$("video, iframe").each((i, elem)=>{let src =$(elem).attr("src");if(!src &&$(elem).attr("data-src")){ src =$(elem).attr("data-src");}if(src){const absoluteUrl =newURL(src, url).href; media.videos.push({url: absoluteUrl,});}});// Extract audios$("audio").each((i, elem)=>{const src =$(elem).attr("src");if(src){const absoluteUrl =newURL(src, url).href; media.audios.push({url: absoluteUrl,});}});// Save media files if saveDir is providedif(options.saveDir){// Create save directory if it doesn't existif(!fs.existsSync(options.saveDir)){ fs.mkdirSync(options.saveDir,{recursive:true});}// Save imagesfor(let i =0; i < media.images.length; i++){const image = media.images[i];try{const imageResponse =await axios.get(image.url,{responseType:"stream",});const imageName =`image_${i}_${path.basename(newURL(image.url).pathname)}`;const imagePath = path.join(options.saveDir, imageName);const writer = fs.createWriteStream(imagePath); imageResponse.data.pipe(writer);awaitnewPromise((resolve, reject)=>{ writer.on("finish",()=>resolve()); writer.on("error", reject);}); image.localPath = imagePath;}catch(error){ console.error(`Error saving image ${image.url}:`, error.message);}}// Save videosfor(let i =0; i < media.videos.length; i++){const video = media.videos[i];try{const videoResponse =await axios.get(video.url,{responseType:"stream",});const videoName =`video_${i}_${path.basename(newURL(video.url).pathname)}`;const videoPath = path.join(options.saveDir, videoName);const writer = fs.createWriteStream(videoPath); videoResponse.data.pipe(writer);awaitnewPromise((resolve, reject)=>{ writer.on("finish",()=>resolve()); writer.on("error", reject);}); video.localPath = videoPath;}catch(error){ console.error(`Error saving video ${video.url}:`, error.message);}}// Save audiosfor(let i =0; i < media.audios.length; i++){const audio = media.audios[i];try{const audioResponse =await axios.get(audio.url,{responseType:"stream",});const audioName =`audio_${i}_${path.basename(newURL(audio.url).pathname)}`;const audioPath = path.join(options.saveDir, audioName);const writer = fs.createWriteStream(audioPath); audioResponse.data.pipe(writer);awaitnewPromise((resolve, reject)=>{ writer.on("finish",()=>resolve()); writer.on("error", reject);}); audio.localPath = audioPath;}catch(error){ console.error(`Error saving audio ${audio.url}:`, error.message);}}}return{ content,length: content.length, url, media,};}catch(error){ console.error("Error fetching website content:", error);thrownewError(`Failed to fetch content from ${url}: ${error.message}`);}}// If run directly, test the functionif(require.main === module){const url = process.argv[2]||"https://example.com";const saveDir = process.argv[3];const options ={};if(saveDir){ options.saveDir = saveDir;}fetchWebsiteContent(url, options).then((result)=>{ console.log("Fetched content:"); console.log(`URL: ${result.url}`); console.log(`Length: ${result.length} characters`); console.log("Content:"); console.log(result.content);// Display media resources console.log("\nMedia resources:");if(result.media.images.length >0){ console.log("Images:"); result.media.images.forEach((image, index)=>{ console.log(`${index +1}. ${image.url} (alt: ${image.alt})`);if(image.localPath){ console.log(` Saved to: ${image.localPath}`);}});}if(result.media.videos.length >0){ console.log("\nVideos:"); result.media.videos.forEach((video, index)=>{ console.log(`${index +1}. ${video.url}`);if(video.localPath){ console.log(` Saved to: ${video.localPath}`);}});}if(result.media.audios.length >0){ console.log("\nAudios:"); result.media.audios.forEach((audio, index)=>{ console.log(`${index +1}. ${audio.url}`);if(audio.localPath){ console.log(` Saved to: ${audio.localPath}`);}});}}).catch((error)=>{ console.error("Error:", error.message);});} module.exports ={ fetchWebsiteContent };

evals

evals.json

{"skill_name":"website-content-fetch","evals":[{"id":1,"prompt":"Fetch content from https://example.com","expected_output":"Should return the text content of example.com","files":[]},{"id":2,"prompt":"Fetch content from a nonexistent domain","expected_output":"Should throw an error about failed to fetch content","files":[]},{"id":3,"prompt":"Fetch content without providing a URL","expected_output":"Should throw an error about URL being required","files":[]}]}

测试

将开发的skill目录website-content-fetch引入支持skill的AI助手或者开发IDE,此处不赘述,只要website-content-fetch放入对应的skills目录,助手或者IDE都可以识别到。

开发的时候,只需要在项目中,和AI工具说使用website-content-fetch skill为我抓取https://www.baidu.com中的内容即可,比如我使用Trae对话,AI会自动发现该工具,并识别scripts的语言识别到node环境会开始install依赖,再用skill执行任务

请添加图片描述


install成功之后,环境准备就绪,就会按照skill开发的功能执行任务了

请添加图片描述
  • 最后再反复测试结果,调试scripts功能,优化逻辑
  • 丰富SKILL内容,为AI下次识别工具,对skill的功能和逻辑有更好的理解,更精确的执行任务

最后附上Github地址
aubrey-skills

Read more

【薅羊毛教程】LLaMaFactory 不用本地跑!免费 GPU,一键微调大模型

【薅羊毛教程】LLaMaFactory 不用本地跑!免费 GPU,一键微调大模型

一、环境 之前介绍过本地部署LLaMaFactory微调平台(https://blog.ZEEKLOG.net/m0_73982863/article/details/159208213?spm=1001.2014.3001.5501),如果你还在为设备问题而烦恼,那就来薅羊毛吧(手动狗头)。 首先注册魔搭社区,绑定个人阿里云账号即可,详情见:https://www.modelscope.cn/my/mynotebook ;然后就可免费获得36小时GPU环境。 8核:CPU有8个核心,主要负责数据的调度和预处理;32GB:内存,数据从硬盘加载后会暂时存放这里;显存24G;(比我自己的老古董好多 T-T) Ubuntu 22.04:Linux操作系统; CUDA 12.8.1:英伟达的并行计算平台。12.8版本意味着它支持最新的RTX

一文通透OpenVLA——在Prismatic VLM(SigLIP、DinoV2、Llama 2)的架构上:基于“下一个token预测技术”预测离散化动作

一文通透OpenVLA——在Prismatic VLM(SigLIP、DinoV2、Llama 2)的架构上:基于“下一个token预测技术”预测离散化动作

前言 当对机器人动作策略的预测越来越成熟稳定之后(比如ACT、比如扩散策略diffusion policy),为了让机器人可以拥有更好的泛化能力,比较典型的途径之一便是基于预训练过的大语言模型中的广泛知识,然后加一个policy head(当然,一开始背后的模型比较简单,比如有用LSTM或MLP——RoboFlamingo) 再之后,便出来了越来越多成熟稳定的专门的VLA模型,比如OpenVLA,再比如近期介绍过过的π0——用于通用机器人控制的VLA模型:一套框架控制7种机械臂(基于PaliGemma和流匹配的3B模型) 1. π0的意义在于,首次用同一套策略/算法操作不同机器人/机械臂,这种基于机器人大模型的「预训练-微调」模式,很快会越来越多(犹如此前大模型革命NLP 其次CV等各模态,目前到了robot领域),算是代表了通用机器人的核心发展方向 2. 且π0 比英伟达的HOVER早一点,当然,同时期的RDT GR2也有这个潜力的,期待这两 后续的更新 一个多月前(本文首发于25年1月),有朋友曾说,一个月内,π0 会开源来着,当时虽然觉得不太可能,但还是抱着期待,可还

llama.cpp重大更新:自带Web UI,性能超越Ollama,本地大模型部署新选择!

llama.cpp重大更新:自带Web UI,性能超越Ollama,本地大模型部署新选择!

Ollama 背后执行推理的核心技术其实是由 llama.cpp 承担的,GGUF 模型格式也是由 llama.cpp 的作者所开发。 现在 llama.cpp 迎来重大更新,它也有了自己的 Web UI,我测试了安装部署和自行打包,很多地方确实比 Ollama 还有方便好用。 官方介绍,优势如下: * 完全免费、开源且由社区驱动 * 在所有硬件上表现出色 * 高级上下文和前缀缓存 * 并行和远程用户支持 * 极其轻量级且内存高效 * 充满活力且富有创造力的社区 * 100% 隐私 使用之前需要先安装 llama.cpp server 我还是喜欢命令行直接安装 ## Winget (Windows)winget install llama.cpp## Homebrew (Mac and Linux)brew install llama.

污泥清淤机器人实践复盘分享

污泥清淤机器人实践复盘分享

污泥清淤机器人实践复盘:从行业痛点看智能化解决方案 在化工、市政、河道治理等众多领域,清淤作业长期面临着安全风险高、效率低下、环境影响大等严峻挑战。传统人工作业方式在有毒有害、密闭缺氧的环境中难以为继,行业对安全、高效、智能的清淤解决方案需求迫切。近年来,以清淤机器人为代表的特种作业装备快速发展,为行业带来了革命性的变化。本文旨在通过实践复盘,深入探讨清淤机器人的应用价值、技术要点与发展趋势。 一、行业痛点催生技术变革 清淤作业,尤其是工业场景下的清淤,绝非简单的体力劳动。行业报告显示,在化工厂、钢铁冶金、污水处理厂等场所,作业环境往往伴随着高浓度有毒有害化学物质、污泥厌氧分解产生的易燃易爆气体,以及密闭空间氧气不足导致的窒息风险。人工清淤事故频发,使得安全规范日益严格,传统作业模式已触及瓶颈。 与此同时,市政管网、水库涵洞、港口航道等受限空间的清淤需求巨大,但空间狭小、环境复杂,人员与大型设备均难以进入。这些痛点共同构成了对“人不能近、人不能及、人不能为”作业场景的精准描述,也成为了推动水下清淤机器人等智能装备从研发走向广泛应用的核心驱动力。 二、清淤机器人的核心技术与