买东西总是刚下单就降价?本文以
price-watch(商品降价监控器) 为例,手把手带你写一个真正有用的 OpenClaw Skill,从零开始到发布 ClawHub,全流程跑通。
为什么是这个选题?
你有没有这种经历:
在京东收藏了一款机械键盘,想等双十一降价再买——然后忘了。三个月后打开一看,价格早就涨回去了,降价那几天你完全不知道。
price-watch 解决的就是这件事:你说一句"帮我盯着这个商品,降到 X 元告诉我",之后的事情完全不用管。
为什么这个方向在 ClawHub 上几乎是空白:
- 现有爬虫工具都是"拿数据",没有做"持续比较 + 主动通知"的完整闭环
- 大多数价格监控工具是独立 App,需要单独安装账号,而不是融入你的 AI 工作流
- 这个场景对技术门槛低、受众广——程序员、买手、普通消费者都用得上
先看效果,再来写代码
完成之后,你只需要对 OpenClaw 说:
帮我监控这个商品的价格:https://item.jd.com/100012043978.html
目标价格:799 元,降到就通知我
OpenClaw 会:
- 抓取当前价格,存入本地
price-history.json - 每隔 6 小时自动检查一次(通过 HEARTBEAT 触发)
- 价格低于 799 元时,在对话里弹出提醒,并生成价格走势摘要
一、环境准备
# Node.js 18+
node -v
# npm 9+
npm -v
# OpenClaw CLI(用于本地测试)
openclaw --version
# Playwright(用于抓取动态页面,京东/淘宝都是 JS 渲染)
npx playwright install chromium
为什么需要 Playwright? 京东、淘宝的商品价格不在 HTML 源码里,是 JavaScript 动态渲染的。
curl或fetch抓到的是空壳,只有真实浏览器渲染后才能拿到价格数字。
二、文件结构
openclaw-skill-price-watch/
├── SKILL.md # Skill 的大脑:告诉 AI 何时用、怎么用
├── scripts/
│ ├── fetch-price.js # 核心脚本:抓取商品价格
│ └── check-alert.js # 比较历史价格,判断是否需要提醒
├── package.json
└── README.md
一个文件一个职责,逻辑清晰,后续扩展也方便。
三、写 SKILL.md(最关键的一步)
SKILL.md 是 Skill 的灵魂文件。OpenClaw 读它,决定"什么时候调用这个 Skill"以及"调用时怎么操作"。它分三段:
---
name: price-watch
version: 1.0.0
author: 你的 GitHub ID
description: 监控商品价格,降到目标价格时主动提醒
tags: [shopping, monitor, price, ecommerce]
---
## Use when
用户说以下任意一种话时,激活这个 Skill:
- "帮我盯着这个商品" / "监控这个链接的价格"
- "这个东西降到 X 元告诉我"
- "我想等降价再买" / "有没有到历史低价"
- 提供了京东/淘宝/亚马逊/拼多多商品链接,并提及"降价"/"价格"/"通知"
## NOT for
- 用户只是问"这个东西多少钱"(单次查询,不需要持续监控)
- 没有提供具体商品链接
- 不涉及价格比较或降价提醒的普通购物建议
## Workflow
执行以下步骤:
### 步骤 1:解析用户输入
从对话中提取:
- `product_url`:商品链接(必须)
- `target_price`:目标价格(可选,未提供则只做记录,不设提醒阈值)
- `check_interval`:检查频率(默认 6 小时)
### 步骤 2:首次抓取
运行 `node scripts/fetch-price.js <product_url>`
将结果(商品名、当前价格、抓取时间)追加写入 `~/.openclaw/price-history.json`
### 步骤 3:立刻比较
如果用户设置了 `target_price`,运行 `node scripts/check-alert.js`
- 如果当前价格 ≤ 目标价格:**立刻告知用户,附商品链接**
- 如果尚未到目标价格:告知用户"已开始监控,当前价格 X 元,目标 Y 元,差 Z 元"
### 步骤 4:设置定时监控
在 HEARTBEAT 配置中注册定时任务:每隔 `check_interval` 自动执行步骤 2 和步骤 3
### 步骤 5:汇报状态
向用户输出:
- 商品名称
- 当前价格 vs 目标价格
- 下次检查时间
- 过去价格走势(如有历史记录,列出最近 5 条)
SKILL.md 的三个关键区域解释:
| 区域 | 作用 | 写坏了会怎样 |
|---|---|---|
Use when | AI 的触发条件判断 | 写太宽泛 → 误触发;写太窄 → 永远不触发 |
NOT for | 排除不应触发的场景 | 不写 → AI 会在不合适的场景也调用 |
Workflow | AI 执行的具体步骤 | 步骤模糊 → AI 自由发挥,结果不可控 |
四、核心脚本:fetch-price.js
// scripts/fetch-price.js
// 用法:node scripts/fetch-price.js <商品URL>
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const os = require('os');
const HISTORY_FILE = path.join(os.homedir(), '.openclaw', 'price-history.json');
// 根据域名选择抓取策略
const PLATFORM_CONFIG = {
'item.jd.com': {
name: '京东',
priceSelector: '.price.J-p',
titleSelector: '.sku-name',
waitFor: '.price.J-p',
},
'detail.tmall.com': {
name: '天猫',
priceSelector: '.tm-price',
titleSelector: '.title-text',
waitFor: '.tm-price',
},
'item.taobao.com': {
name: '淘宝',
priceSelector: '.tb-rmb-num',
titleSelector: '.title-text',
waitFor: '.tb-rmb-num',
},
'www.amazon.cn': {
name: '亚马逊',
priceSelector: '#priceblock_ourprice, .a-price .a-offscreen',
titleSelector: '#productTitle',
waitFor: '#priceblock_ourprice',
},
};
function detectPlatform(url) {
const hostname = new URL(url).hostname;
for (const [domain, config] of Object.entries(PLATFORM_CONFIG)) {
if (hostname.includes(domain)) return config;
}
// 通用兜底策略
return {
name: '未知平台',
priceSelector: '[class*="price"]:not([class*="original"]):not([class*="del"])',
titleSelector: 'h1, [class*="title"]',
waitFor: null,
};
}
async function fetchPrice(productUrl) {
const platform = detectPlatform(productUrl);
console.log(`[price-watch] 正在抓取 ${platform.name} 商品价格...`);
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const context = await browser.newContext({
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1280, height: 800 },
});
const page = await context.newPage();
try {
await page.goto(productUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
if (platform.waitFor) {
await page.waitForSelector(platform.waitFor, { timeout: 15000 }).catch(() => {});
}
// 等待价格渲染
await page.waitForTimeout(2000);
const priceText = await page.$eval(
platform.priceSelector,
(el) => el.textContent.trim()
).catch(() => null);
const title = await page.$eval(
platform.titleSelector,
(el) => el.textContent.trim()
).catch(() => '未知商品');
if (!priceText) {
throw new Error('未能抓取到价格,页面结构可能已更新');
}
// 清洗价格:提取数字(支持 "¥799.00"、"799.00元" 等格式)
const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
if (isNaN(price)) {
throw new Error(`价格解析失败,原始文本:${priceText}`);
}
const record = {
url: productUrl,
platform: platform.name,
title: title.slice(0, 80), // 截断过长标题
price,
timestamp: new Date().toISOString(),
};
// 追加写入历史文件
saveRecord(record);
console.log(`[price-watch] 抓取成功:${record.title}`);
console.log(`[price-watch] 当前价格:¥${price}`);
console.log(JSON.stringify(record));
return record;
} finally {
await browser.close();
}
}
function saveRecord(record) {
// 确保目录存在
const dir = path.dirname(HISTORY_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
let history = [];
if (fs.existsSync(HISTORY_FILE)) {
try {
history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
} catch {
history = [];
}
}
history.push(record);
// 每个商品最多保留 90 条历史,防止文件无限膨胀
const urlHistory = history.filter((r) => r.url === record.url);
if (urlHistory.length > 90) {
const toRemove = urlHistory.length - 90;
let removed = 0;
history = history.filter((r) => {
if (r.url === record.url && removed < toRemove) {
removed++;
return false;
}
return true;
});
}
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
}
// 入口
const url = process.argv[2];
if (!url) {
console.error('用法:node scripts/fetch-price.js <商品URL>');
process.exit(1);
}
fetchPrice(url).catch((err) => {
console.error(`[price-watch] 抓取失败:${err.message}`);
process.exit(1);
});
五、提醒脚本:check-alert.js
// scripts/check-alert.js
// 用法:node scripts/check-alert.js <商品URL> <目标价格>
const fs = require('fs');
const path = require('path');
const os = require('os');
const HISTORY_FILE = path.join(os.homedir(), '.openclaw', 'price-history.json');
function checkAlert(productUrl, targetPrice) {
if (!fs.existsSync(HISTORY_FILE)) {
console.log('暂无历史数据,请先运行 fetch-price.js');
return;
}
const history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
const urlHistory = history
.filter((r) => r.url === productUrl)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (urlHistory.length === 0) {
console.log(`未找到该商品的监控记录:${productUrl}`);
return;
}
const latest = urlHistory[0];
const currentPrice = latest.price;
const diff = currentPrice - targetPrice;
// 输出结构化结果,供 OpenClaw 解析
const result = {
title: latest.title,
platform: latest.platform,
url: productUrl,
currentPrice,
targetPrice,
diff: parseFloat(diff.toFixed(2)),
triggered: currentPrice <= targetPrice,
lastChecked: latest.timestamp,
recentHistory: urlHistory.slice(0, 5).map((r) => ({
price: r.price,
time: r.timestamp,
})),
lowestEver: Math.min(...urlHistory.map((r) => r.price)),
};
console.log(JSON.stringify(result, null, 2));
if (result.triggered) {
console.log(`\n🎯 触发提醒!${latest.title} 当前价格 ¥${currentPrice},已达到目标价 ¥${targetPrice}`);
} else {
console.log(`\n⏳ 尚未降价。当前 ¥${currentPrice},目标 ¥${targetPrice},还差 ¥${Math.abs(diff).toFixed(2)}`);
}
}
const [, , url, targetPriceStr] = process.argv;
if (!url || !targetPriceStr) {
console.error('用法:node scripts/check-alert.js <商品URL> <目标价格>');
process.exit(1);
}
checkAlert(url, parseFloat(targetPriceStr));
六、package.json
{
"name": "openclaw-skill-price-watch",
"version": "1.0.0",
"description": "OpenClaw Skill:监控商品价格,降到目标价格时主动提醒",
"main": "index.js",
"keywords": [
"openclaw-skill",
"price",
"monitor",
"shopping",
"ecommerce"
],
"author": "你的 GitHub ID",
"license": "MIT",
"scripts": {
"test": "node scripts/fetch-price.js https://item.jd.com/100012043978.html"
},
"dependencies": {
"playwright": "^1.43.0"
},
"engines": {
"node": ">=18.0.0"
}
}
包名必须以
openclaw-skill-开头,这是 ClawHub 识别 Skill 包的命名规范,不符合则无法上架。
七、本地测试
# 安装依赖
npm install
# 安装 Chromium(首次需要)
npx playwright install chromium
# 测试抓取京东价格
node scripts/fetch-price.js https://item.jd.com/100012043978.html
# 测试降价提醒(假设目标价 799 元)
node scripts/check-alert.js https://item.jd.com/100012043978.html 799
# 在 OpenClaw 中整体测试
openclaw "帮我监控这个商品:https://item.jd.com/100012043978.html,降到 799 元告诉我"
常见调试问题速查:
| 问题 | 原因 | 解决方法 |
|---|---|---|
价格返回 NaN | CSS 选择器失效,平台改版 | 打开 DevTools 重新找价格元素 |
| 超时 timeout | 网速慢或反爬拦截 | 增加 waitForTimeout,或加代理 |
| 抓到的是划线价 | 选择器匹配到原价而非促销价 | 细化选择器,排除 .del / .original 类 |
| 历史文件越来越大 | 商品过多 | 脚本已内置每商品最多 90 条的自动裁剪 |
八、发布到 ClawHub
第一步:发布到 npm
# 登录 npm(没有账号先去 npmjs.com 注册)
npm login
# 发布
npm publish --access public
发布成功后,你的包地址是:
https://www.npmjs.com/package/openclaw-skill-price-watch
第二步:在 ClawHub 登记
打开 clawhub.com/submit,填写:
| 字段 | 内容 |
|---|---|
| npm 包名 | openclaw-skill-price-watch |
| 分类 | productivity / tools |
| 简介 | 监控商品价格,达到目标价格时主动提醒 |
| 截图/演示 | 可附命令行运行截图 |
1-2 小时后自动审核,审核通过即可被所有人通过 clawhub install openclaw-skill-price-watch 安装。
九、进阶方向
完成基础版之后,你可以继续往这几个方向扩展:
1. 多平台价格对比
同一个商品关键词,同时查询京东/淘宝/拼多多的价格,自动选出最低价平台推荐给用户。
2. 微信/飞书推送
价格触达阈值时,不只是在 OpenClaw 对话里通知,同时通过 webhook 推送到微信群或飞书机器人。
// 飞书 webhook 推送示例
async function notifyFeishu(message) {
await fetch(process.env.FEISHU_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ msg_type: 'text', content: { text: message } }),
});
}
3. 价格走势图
把历史价格数据导出成 CSV,或者用 echarts-node-canvas 生成一张折线图,直接在对话里展示价格曲线。
4. 历史最低价对比
在抓取当前价格时,同步与 lowestEver 比较——如果当前价格是历史最低,无论是否达到目标价,都值得提醒。
最后一句话
price-watch 这个 Skill 本质上做了一件事:把"我记得要盯价格"这件需要人类工作记忆的事,彻底外包给 AI。
你不需要每天打开购物 App 检查,不需要记住什么时候收藏了什么东西——你只要表达过一次意图,AI 替你持续盯着,直到事情发生。
这才是 AI Agent 真正该做的事。