Clawshop 自动带货客户端 skill。装上 → 登录账号密码 → /clawshop 直接用,零配置。
---
name: clawshop
version: "1.3.3"
description: Clawshop 自动带货客户端 skill。装上 → 登录账号密码 → /clawshop 直接用,零配置。
metadata: {"openclaw":{"emoji":"🛒"}}
command:
name: clawshop
description: "Clawshop 自动带货(SaaS)"
descriptionLocalizations:
zh-CN: "Clawshop 自动带货"
args:
- name: url
description: "指定视频URL(抖音/TikTok等),留空则推荐热门"
captureRemaining: true
---
# Clawshop Skill
**Clawshop 自动带货客户端**。装上即用,账号密码登录后所有 AI key、采集系统、视频生成全部由我方服务器统一管理,用户零配置。
> **跟 `clawshop` skill 的区别:** clawshop 是 SaaS 客户端,走 `/api/v1/*` + Bearer token + 短轮询事件队列。
## ⚠️ 铁律
- **收到触发后,你唯一允许做的事就是调 exec 执行脚本。** 绝对不允许自己编任何文字回复。不说“我先读一下”“让我看看”“收到”“已启动”“请登录”等。所有用户可见的文字100%由脚本内部发出,LLM 永远只调 exec 然后 NO_REPLY。你看到的任何文案模板都是脚本内部用的,不是给你复制粘贴的。
- **发完审核卡片后绝对不发任何额外消息。** 不发“分镜出来了”“等老板点按钮”“已确认生成中”等。
- **每个流程最多 1 次 exec + 1 次 message。** 不允许拆分。
- **遇到异常(超时/失败)唯一允许的动作:告诉老板"XX失败了,原因是YY",然后等指示。**
- **不偷偷换方案、不改卡片格式、不改流程。**
## ⛔ 龙虾不准解读脚本 stdout(铁律)
**龙虾只负责调脚本 + NO_REPLY。所有给老板看的提示(未登录/登录失败/网络错误/账号密码错误等)一律由脚本内部 `ct.send_text()` 发出。**
- 龙虾**不准**根据脚本 stdout 的 `{"ok": false, "reason": "..."}` 自己编话给老板
- 龙虾**不准**把 `reason: not_logged_in` 解读成"接口 404"、"服务挂了"、"请登录"等任何文字
- 龙虾**不准**根据 `recommend_status` / `ack_status` 等数字自己判断状态
- 脚本的 stdout 是给程序/调试看的,不是给老板看的
- 老板要看的所有文字,**必须**由脚本通过 `send_text` 主动推到 Discord
**正确流程:**
```
收到触发 → exec 调脚本 → 脚本内部决定发什么消息 → NO_REPLY
```
**❗ 例外:Discord 斜杠命令触发时,不能 NO_REPLY!**
Discord 斜杠命令要求 3 秒内必须有可见回复,否则显示"Command produced no visible reply"。
当触发来源是 `/clawshop` 斜杠命令时:
```
收到 /clawshop → 发一条短 ack 文本(给 Discord 斜杠命令回复)→ exec 调脚本 → 回复结束(不再补话)
```
这句 ack 是给 Discord interaction 的回复,不算"额外消息",不违反铁律。
**错误示范(绝对禁止):**
```
收到触发 → exec 调脚本 → 看 stdout reason → 自己组织语言告诉老板
```
**为什么这样要求:** 以后会有多个用户用这个 skill,逻辑必须沉淀在代码里。龙虾每次解读 stdout 编话,不同用户/不同会话给出的回答就不一致,且代码无法演进。所有提示语统一由脚本管理才能保持一致行为。
**❗ 例外 2:stdout 以 `OUTPUT_AS_REPLY:` 开头时**
当脚本 stdout 第一行以 `OUTPUT_AS_REPLY:` 开头时,龙虾必须把后面的文本(去掉前缀)**原样**作为回复发出去。这不是“解读”也不是“组织语言”,是机械转发。
- 用于 webchat 等无法走 message tool 主动发送的场景(比如用户还没配 Discord / 飞书)。
- 脚本负责写死文字,龙虾不修改、不添加、不总结、不拆分。
- 转发后不再补任何话。
---
## 一、整体架构
```
用户 OpenClaw 实例
├── clawshop skill(本 skill)
│ ├── data/
│ │ ├── token.json ← 登录后存
│ │ ├── card_map.json ← 短编号 ↔ video_id 映射
│ │ └── processed_events.json ← 已处理事件去重
│ └── lib/ ← 全部脚本
└── 用户配置的 Discord 频道(OpenClaw 自带)
→ data-service(我方服务器,单一来源)
└── /api/v1/* 接口 + 事件队列
```
---
## 二、用户使用流程
### 2.0 安装后命令自动注册
用户 `clawhub install clawshop` 装上 skill 后,需要让 `/clawshop` 在用户的 Discord 服务器里立刻生效。
**为什么不能依赖 OpenClaw 默认同步?**
OpenClaw 默认把 skill 命令同步为 Discord **global command**。Global command 在 Discord 端有最长 1 小时缓存,用户装完看不到命令体验差。
**解决方案:首次触发自动注册 guild command(无缓存,立刻生效)**
用户安装完 skill 后,在 Discord 里随便发一句"clawshop"或"带货"(不需要斜杠),LLM 会识别到这个 skill 并触发 `trigger.mjs`。`trigger.mjs` 首次运行时会自动调用 `register-discord.mjs` 把 `/clawshop` 注册为 guild command,注册完用户立刻能在输入框看到 `/clawshop`。
**只执行一次:** `data/registered` 标记文件存在后不再重复注册。如需重新注册(如 bot 加入新 guild),删除 `data/registered` 后再触发一次即可。
**命令:**
```
node ./lib-js/register-discord.mjs
```
**输出示例:**
```json
{
"ok": true,
"app_id": "1496036936996093992",
"guild_count": 1,
"results": [
{
"guild": "用户的服务器",
"guild_id": "...",
"status": 201,
"ok": true,
"cmd_id": "..."
}
]
}
```
**从 OpenClaw 主会话中调用:**
老板可以说"注册 clawshop 命令"或"注册服务器命令",龙虾直接 `exec: node ./lib-js/register-discord.mjs` 跑一次即可。
### 2.1 首次(脚本内部自己处理,龙虾只负责调脚本)
```
1. /clawshop → 龙虾 exec trigger.mjs ""
2. trigger.mjs 检测 token.json 不存在 → 自己 send_text("🔑 欢迎使用 Clawshop,请回复账号和密码:`用户名 密码`")
3. 老板回:alice ****
4. 龙虾 exec trigger.mjs "alice ****"
5. trigger.mjs POST /api/v1/auth/login → token 存本地 → 自己 send_text("✅ 登录成功,开始推荐...")
6. trigger.mjs 自动 POST /api/v1/recommend + 启动 cron 轮询
7. cron 拿到 review_1 事件 → 发卡片
```
### 2.2 后续
```
1. /clawshop → 龙虾 exec trigger.mjs ""
2. trigger.mjs 检测有 token → 直接 POST /api/v1/recommend → 启动轮询
3. cron 拿到事件 → 发卡片 → 老板点按钮 → 调对应 API → 继续轮询
4. 流程结束(review_2 通过/放弃)→ 停止轮询
```
**注意:** Token 自动保存在 `data/token.json`,登录一次后永不需要重复登录,除非服务端把 token 标记失效(401,脚本会自动清本地 token 并 send_text 提示重新登录)。
---
## 三、触发方式
响应:
- `/clawshop` Discord 斜杠命令(可选 `url:` 参数)
- `/推荐` 命令
- 文字消息含 URL(如 `推荐 https://...`)
- 系统提示 `Use the "clawshop" skill for this request.`
- 含"xxx yyy"格式的消息(两个词=账号密码)
- **纯文字 `clawshop`、`带货`、`开始`**(无斜杠,适用于首次安装后还没注册斜杠命令的场景)
**首次触发自动注册:**
用户安装 skill 后第一次触发(无论通过斜杠命令还是纯文字),trigger.mjs 会自动调用 `register-discord.mjs` 把 `/clawshop` 注册为 guild command(立即生效,无缓存)。此后用户就能直接用 `/clawshop` 了。只执行一次,标记文件 `data/registered` 存在后不再重复注册。
### 3.1 URL 提取规则
```javascript
import re
def extract_url(msg: str) -> str | None:
if not msg:
return None
s = msg.strip()
s = re.sub(r'^\s*(/clawshop|/推荐|推荐|来一个)\s*', '', s, flags=re.I)
s = re.sub(r'^\s*url\s*[::]\s*', '', s, flags=re.I)
s = s.strip().strip('<>').strip('"\'「」『』').strip()
if re.match(r'^https?://', s, flags=re.I):
return s
return None
```
### 3.2 登录指令识别
老板首次会发:`用户名 密码`(两个词空格隔开)。
```javascript
import re
m = re.match(r'^\s*(登录|login)\s+(\S+)\s+(\S+)\s*$', msg)
if m:
username, password = m.group(2), m.group(3)
```
### 3.3 触发统一脚本
**所有 exec 命令必须带 `workdir` 参数,指向本 skill 目录(即 SKILL.md 所在目录)。**
**必须传入渠道参数!** 第二个参数是消息来源渠道(discord/feishu/telegram/slack等),第三个参数是 target(如飞书的 `user:ou_xxx` 或 discord 的 `channel:xxx`),确保回复发往正确的渠道和对象。
```
exec: node ./lib-js/trigger.mjs "<老板原始消息文本>" "<channel>" "<target>"
workdir: <本 skill 目录>
```
实际调用时,模型应该用 exec 工具的 workdir 参数:
```json
{"command": "node ./lib-js/trigger.mjs \"<消息>\" \"<channel>\" \"<target>\"", "workdir": "<skill目录绝对路径>"}
```
**channel 值从当前会话的 inbound metadata 获取(如 `"channel": "feishu"` 或 `"channel": "discord"`)。**
**target 值从当前会话的 deliveryContext 获取(如 `"to": "user:ou_xxx"`)。如果不确定 target,传空字符串让脚本自动检测。**
Skill 目录路径可通过 `SKILL.md` 所在位置确定,不同系统不同:
- Windows: `C:\Users\xxx\openclaw\skills\clawshop-pro`
- macOS: `/Users/xxx/openclaw/skills/clawshop-pro`
- Linux: `/home/xxx/openclaw/skills/clawshop-pro`
```
脚本内部:
1. **解析消息**
- 是 `xxx yyy`(两个词)→ 走登录流程
- 否 → 走推荐流程
2. **登录流程**
- POST `/api/v1/auth/login` `{username, password}`
- 成功 → 存 token 到 `data/token.json` → 发"✅ 登录成功,开始推荐"消息 → 继续走推荐流程
- 失败 → 发"❌ 账号密码错误"消息 → 退出
3. **推荐流程**
- 检查本地 `token.json`:不存在 → 发"请回复账号和密码:`用户名 密码`" → 退出
- 提取 URL(同 3.1 规则)
- 立即发 ack 文本到 Discord(解决斜杠命令 3s 超时)
- 无 URL:`⏳ 已启动热门推荐,捐到合适的视频再给您发审核卡片。`
- 有 URL:`⏳ 指定视频已接收,正在抓取分析,完成后给您发审核卡片。\n<url>`
- POST `/api/v1/recommend` `{}` 或 `{"video_url": "..."}`
- **启动 cron 轮询任务**(见第六节)
- 脚本退出,本轮 NO_REPLY
**绝对禁止:** 直接 `requests.post(...)` 绕开脚本;拆成多次 tool call。
---
## 四、API 客户端
所有 `/api/v1/*` 接口共用同一个客户端(`lib-js/api.mjs`),自动加 `Authorization: Bearer <token>` header。
### 4.1 业务接口(9 个)
| 函数 | 路径 | 说明 |
|---|---|---|
| `login(u, p)` | `POST /api/v1/auth/login` | 拿 token |
| `recommend(url=None)` | `POST /api/v1/recommend` | 启动推荐 |
| `task_approve(vid, step)` | `POST /api/v1/tasks/{vid}/approve` 或 `/{step}/approve` | 通过 |
| `task_reject(vid, step)` | `POST /api/v1/tasks/{vid}/reject` | 跳过/放弃 |
| `task_change_product(vid)` | `POST /api/v1/tasks/{vid}/change-product` | 换商品 |
| `task_storyboard_redo(vid)` | `POST /api/v1/tasks/{vid}/storyboard/redo` | 重做分镜 |
| `task_video_redo(vid)` | `POST /api/v1/tasks/{vid}/video/redo` | 重做视频 |
### 4.2 事件接口(2 个)
| 函数 | 路径 | 说明 |
|---|---|---|
| `events_pending()` | `GET /api/v1/events/pending` | 拉积压事件 |
| `event_ack(eid)` | `POST /api/v1/events/{eid}/ack` | 确认消费 |
### 4.3 错误处理
- `401 invalid_token` → 删本地 token.json,发"❌ 身份已过期,请重新发送账号和密码:`用户名 密码`"
- `403 forbidden` → 发"❌ 操作越权" + 错误内容
- `5xx` → 发"❌ 服务器错误,请稍后重试"
- 超时 → 发"❌ 请求超时"
---
## 五、事件处理(核心)
### 5.1 事件类型
事件格式:
| event_type | 触发 | data 字段 | 用户可见提示(脚本内部发) |
|---|---|---|---|
| `review_1` | 一审就绪 | video_id, video_url, author, view_count, score, summary, product_name, product_url, product_image_url | 发一审卡片(`send-review-1.mjs`) |
| `review_1_5` | 分镜就绪 | video_id, storyboard_url(或 error)| 有 storyboard_url → 发分镜卡片;有 error → `⚠️ 视频 #{n} 分镜生成失败:{error}` |
| `review_2` | 视频就绪 | video_id, video_url(或 error)| 有 video_url → 发视频卡片;有 error → `⚠️ 视频 #{n} 生成失败:{error}` |
| `published` | 发布完成 | video_id, platform, status | `✅ 视频 #{n} 已成功发布到 {platform}!` |
| `no_match` | 没找到 | (空)| `🙅 这轮没找到合适的视频,稍后再试。` |
| `error` | 异常 | error_type, message | `⚠️ 服务端异常:{message}` |
### 5.1.1 错误事件特殊处理
- `error_type: "quota_exhausted"` → `⚠️ API余额不足,无法继续生成。请充值后重试。\n错误详情:{error内容}`
- 其他 error_type → `⚠️ 服务端异常:{message}`
**所有提示文案由脚本内部 `ct.send_text()` 发出,龙虾不准自己编。**
### 5.2 事件处理脚本
```
exec: node ./lib-js/poll-events.mjs
```
**轮询脚本职责(一次 exec 干完):**
1. 调 `events_pending()` 拉所有未处理事件
2. 对每个事件:
- 检查 `processed_events.json`,处理过 → 直接 ack 跳过
- 没处理过 → 按 event_type 分发到对应 send-review-*.mjs / 处理函数
- 处理成功 → 写入 processed_events + 调 ack
3. 检查是否流程结束(review_2 通过、所有 reject、no_match)→ 停止 cron
4. 输出 JSON 摘要:`{"processed": N, "stopped": true/false}`
### 5.3 子脚本
- `lib-js/send-review-1.mjs` - 发一审卡片
- `lib-js/send-review-1-5.mjs` - 发分镜卡片
- `lib-js/send-review-2.mjs` - 发视频卡片
- `lib-js/handle-button.mjs` - 按钮回调
- `lib-js/card-tools.mjs` - 卡片工具(卡片工具)
---
## 六、Cron 轮询任务
### 6.1 启动
`lib-js/trigger.mjs` 在推荐启动后**注册一个 cron job**:
```javascript
# 通过 OpenClaw gateway HTTP API 注册
POST http://127.0.0.1:18789/tools/invoke
Body: {
"tool": "cron",
"args": {
"action": "add",
"job": {
"name": "clawshop-poll",
"schedule": {"kind": "every", "everyMs": 5000},
"payload": {
"kind": "agentTurn",
"message": "Clawshop 轮询事件: 执行 `node ./lib-js/poll-events.mjs`,有事件就处理;处理完后回复 NO_REPLY。",
"lightContext": true
},
"deleteAfterRun": false,
"delivery": {"mode": "none"}
}
}
}
```
**关键参数:**
- `everyMs: 5000` - 每 5 秒
- `lightContext: true` - 不带历史上下文,省 token
- `delivery.mode: "none"` - 不发系统消息,由脚本自己发卡片
- `name: clawshop-poll` - 固定 ID 方便后续 update/remove
### 6.2 自动停止
**触发停止的条件:**
1. 流程结束事件(`review_2 approve` / `published` / `reject` 全部 / `no_match`)
2. 启动超过 15 分钟(max_duration_seconds)
`poll-events.mjs` 内部检查后调:
```javascript
POST http://127.0.0.1:18789/tools/invoke
Body: {
"tool": "cron",
"args": {"action": "remove", "id": "clawshop-poll"}
}
```
### 6.3 启动时间记录
`data/poll_state.json`:
```json
{"started_at": 1716180000, "active": true}
```
每次 `poll-events.mjs` 启动时检查 `now - started_at > 900` → 强制停止 cron。
---
## 七、按钮回调
收到 `Clicked "✅ 做 #4"` 等按钮消息:
```
exec: node ./lib-js/handle-button.mjs "<clicked_label>" "<channel>"
```
**脚本内部:**
1. 正则提取 `#<short_id>` → 查 card_map → 拿 video_id 和 step
2. 按按钮前缀映射到 action,**先发进度消息(不许改)**
3. 调对应的 API(不再是 `/clawshop/callback`,而是 `/api/v1/tasks/.../xxx`):
| 按钮 | 阶段 | action | 进度消息(老板可见) | API 调用 |
|---|---|---|---|---|
| ✅ 做 | review_1 | approve | `⏳ 视频 #{n} 开始生成分镜图,请稍候...` | `task_approve(vid, "review_1")` → `POST /api/v1/tasks/{vid}/approve` |
| ⏭️ 换一个 | review_1 | reject + recommend | 无(不发进度) | `task_reject(vid)` + `recommend()` |
| 🔄 换商品 | review_1 | change_product | `🔄 正在重新匹配商品...` | `task_change_product(vid)` |
| ✅ 分镜通过 | review_1_5 | approve | `⏳ 分镜已确认,正在生成视频提示词和视频,预计8-10分钟...` | `task_approve(vid, "review_1_5")` → `POST /api/v1/tasks/{vid}/storyboard/approve` |
| 🔄 重新生成 | review_1_5 | redo | `🔄 正在重新生成分镜图...` | `task_storyboard_redo(vid)` |
| ❌ 放弃 | review_1_5/review_2 | reject | `❌ 已放弃视频 #{n}` | `task_reject(vid)` + 删 card_map + 停 cron |
| ✅ 通过 | review_2 | approve | `📤 视频 #{n} 正在提交发布...` | `task_approve(vid, "review_2")` → `POST /api/v1/tasks/{vid}/video/approve` |
| 🔄 重做 | review_2 | redo | `🔄 视频 #{n} 重新生成中...` | `task_video_redo(vid)` |
4. 按钮 review_2 approve / reject 全部 → 删 card_map + 停 cron
5. 按钮回调后**继续 cron 轮询**(等下一个事件)
---
## 八、本地状态文件
| 文件 | 用途 | 示例 |
|---|---|---|
| `data/token.json` | 登录 token | `{"token": "abc...", "username": "alice"}` |
| `data/card_map.json` | 短编号 ↔ video_id | `{"date": "2026-05-20", "next_id": 3, "cards": {...}}` |
| `data/processed_events.json` | 已 ack 的 event_id 集合 | `{"ids": [1001, 1002, 1003]}` |
| `data/poll_state.json` | 轮询启动时间 | `{"started_at": 1716180000, "active": true}` |
---
## 九、消息处理规则
| 收到的消息 | 动作 |
|---|---|
| `/clawshop` / `/推荐` / `推荐 URL` / 裸 URL | 触发 `lib-js/trigger.mjs` |
| `xxx yyy`(账号 密码)| 触发 `lib-js/trigger.mjs`(脚本内部识别)|
| `Clicked "..."` 按钮回调 | 触发 `lib-js/handle-button.mjs` |
### 9.1 按钮回调必须先回复再执行(铁律)
Discord 按钮点击后只给 **3 秒**响应窗口。`handle-button.mjs` 要调 data-service API,经常超过 3 秒。
**正确流程(所有按钮/命令都遵守):**
```
收到 Clicked "..." 或 /clawshop 或登录消息 → 立刻回复 "✅ 收到"(占住 3s 窗口) → 然后 exec 对应脚本 → NO_REPLY
```
**铁律:LLM 回复永远只写 `✅ 收到`,绝对不允许写具体进度/提示文案,避免与脚本重复发送。**
**示例:**
```
用户: Clicked "✅ 做 #3"
助手: ⏳ 视频 #3 开始生成分镜图,请稍候...
[exec: node ./lib-js/handle-button.mjs "✅ 做 #3"]
NO_REPLY
```
```
用户: Clicked "⏭️ 换一个 #5"
助手: ✅ 收到
[exec: node ./lib-js/handle-button.mjs "⏭️ 换一个 #5"]
NO_REPLY
```
**绝对禁止:** 先调脚本等结果再回复。这会导致 Discord 显示"该交互失败"。
| Cron `Clawshop 轮询事件: ...` | 触发 `lib-js/poll-events.mjs` |
| 系统提示 `Use the "clawshop" skill ...` | 按上下文判断走哪个脚本 |
| HEARTBEAT_OK / inter-session announce | NO_REPLY |
---
## 十、隔离规则
- **绝不调 `/recommend` `/clawshop/callback`,只调 `/api/v1/*`**
clawshop 完全独立运行。
---
## 十一、依赖
- data-service(地址在 `config.json` 的 `data_service.base_url`)
- OpenClaw gateway(`http://127.0.0.1:18789`)
- OpenClaw cron 工具
- OpenClaw message 工具(发 Discord 卡片)
- 用户配置的 Discord(自带,不依赖特定频道 ID - 由 OpenClaw 路由)
---
## 十二、注意事项(血泊教训)
1. **token 失效后必须删本地 token.json 再提示老板**,不能光提示不删。
2. **processed_events.json 防重复发卡片**,必须先写文件再 ack(保证 ack 失败也不会重发)。
3. **cron job 启动后必须能停**:流程结束、超时、报错都要停,避免无限轮询烧 token。
4. **lightContext: true** 必须设,否则每次轮询都加载全部上下文,token 暴涨。
5. **登录消息不要 echo 密码**,"已收到登录请求"就够了。
6. **审核卡片走老板自己的 Discord**(OpenClaw message 工具 channel: discord,不指定 channel_id,让 OpenClaw 自动路由到用户配置的频道)。
7. **inter-session 来的 HEARTBEAT_OK / Agent-to-agent announce 消息,一律回 `NO_REPLY`。** 回任何其他文字都会被发到 Discord 频道里污染老板的屏幕。
8. **收到事件但数据不完整(如 storyboard_url 为空),且没有 error 字段,静默跳过。** 不要跟老板说"还没生成好"之类的废话。
9. **任何 review_1 / review_1_5 / review_2 事件进来 → 必须立即调对应 send-review-*.mjs 发卡片。绝对不允许做"是不是重复"的判断,不允许跳过。** 即使同一个 video_id 反复出现(比如换商品后重推 review_1),也是新卡片,必须发。
10. **按钮回调必须查映射文件确认 video_id,不能猜。** 查不到就问老板,不要自己猜。
---
don't have the plugin yet? install it then click "run inline in claude" again.