Install
openclaw skills install clawshop-proClawshop 自动带货客户端 skill。装上 → 登录账号密码 → /clawshop 直接用,零配置。
openclaw skills install clawshop-proClawshop 自动带货客户端。装上即用,账号密码登录后所有 AI key、采集系统、视频生成全部由我方服务器统一管理,用户零配置。
跟
clawshopskill 的区别: clawshop 是 SaaS 客户端,走/api/v1/*+ Bearer token + 短轮询事件队列。
龙虾只负责调脚本 + NO_REPLY。所有给老板看的提示(未登录/登录失败/网络错误/账号密码错误等)一律由脚本内部 ct.send_text() 发出。
{"ok": false, "reason": "..."} 自己编话给老板reason: not_logged_in 解读成"接口 404"、"服务挂了"、"请登录"等任何文字recommend_status / ack_status 等数字自己判断状态send_text 主动推到 Discord正确流程:
收到触发 → exec 调脚本 → 脚本内部决定发什么消息 → NO_REPLY
❗ 例外:Discord 斜杠命令触发时,不能 NO_REPLY!
Discord 斜杠命令要求 3 秒内必须有可见回复,否则显示“Command produced no visible reply”。
当触发来源是 /clawshop 斜杠命令时:
收到 /clawshop → 发一条短 ack 文本(给 Discord 斜杠命令回复)→ exec 调脚本 → 回复结束(不再补话)
ack 文本模板(必须用这个,不许改):
🔑 还未登录 Clawshop。请回复:\登录 用户名 密码``⏳ 已启动热门推荐,带到合适的视频再给您发审核卡。⏳ 指定视频已接收,正在抓取分析,完成后给您发审核卡。✅ 登录成功,开始推荐...❌ 账号密码错误,请确认后重试。ack 后不再补任何进度消息,等 cron 轮询拿到事件后自动发卡片。
这句 ack 是给 Discord interaction 的回复,不算“额外消息”,不违反铁律。
错误示范(绝对禁止):
收到触发 → exec 调脚本 → 看 stdout reason → 自己组织语言告诉老板
为什么这样要求: 以后会有多个用户用这个 skill,逻辑必须沉淀在代码里。龙虾每次解读 stdout 编话,不同用户/不同会话给出的回答就不一致,且代码无法演进。所有提示语统一由脚本管理才能保持一致行为。
用户 OpenClaw 实例
├── clawshop skill(本 skill)
│ ├── data/
│ │ ├── token.json ← 登录后存
│ │ ├── card_map.json ← 短编号 ↔ video_id 映射
│ │ └── processed_events.json ← 已处理事件去重
│ └── lib/ ← 全部脚本
└── 用户配置的 Discord 频道(OpenClaw 自带)
→ data-service(我方服务器,单一来源)
└── /api/v1/* 接口 + 事件队列
用户 clawhub install clawshop 装上 skill 后,需要让 /clawshop 在用户的 Discord 服务器里立刻生效。
为什么不能依赖 OpenClaw 默认同步? OpenClaw 默认把 skill 命令同步为 Discord global command。Global command 在 Discord 端有最长 1 小时缓存,用户装完看不到命令体验差。
解决方案:guild command(无缓存,立刻生效)
skill 提供 lib/register_discord.py,调 Discord REST API 把命令注册到 bot 加入的所有 guild,注册完用户立刻能在输入框看到 /clawshop。
运行时机:
命令:
python ./lib/register_discord.py
输出示例:
{
"ok": true,
"app_id": "1496036936996093992",
"guild_count": 1,
"results": [
{
"guild": "用户的服务器",
"guild_id": "...",
"status": 201,
"ok": true,
"cmd_id": "..."
}
]
}
从 OpenClaw 主会话中调用:
老板可以说“注册 clawshop 命令”或“注册服务器命令”,龙虾直接 exec: python ./lib/register_discord.py 跑一次即可。
1. /clawshop → 龙虾 exec trigger.py ""
2. trigger.py 检测 token.json 不存在 → 自己 send_text("🔑 请登录:登录 用户名 密码")
3. 老板回:登录 alice ****
4. 龙虾 exec trigger.py "登录 alice ****"
5. trigger.py POST /api/v1/auth/login → token 存本地 → 自己 send_text("✅ 登录成功,开始推荐...")
6. trigger.py 自动 POST /api/v1/recommend + 启动 cron 轮询
7. cron 拿到 review_1 事件 → 发卡片
1. /clawshop → 龙虾 exec trigger.py ""
2. trigger.py 检测有 token → 直接 POST /api/v1/recommend → 启动轮询
3. cron 拿到事件 → 发卡片 → 老板点按钮 → 调对应 API → 继续轮询
4. 流程结束(review_2 通过/放弃)→ 停止轮询
注意: Token 自动保存在 data/token.json,登录一次后永不需要重复登录,除非服务端把 token 标记失效(401,脚本会自动清本地 token 并 send_text 提示重新登录)。
响应:
/clawshop Discord 斜杠命令(可选 url: 参数)/推荐 命令推荐 https://...)Use the "clawshop" skill for this request.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
老板首次登录会发:登录 用户名 密码 或 login user pass。
import re
m = re.match(r'^\s*(登录|login)\s+(\S+)\s+(\S+)\s*$', msg)
if m:
username, password = m.group(2), m.group(3)
所有 exec 命令必须带 workdir 参数,指向本 skill 目录(即 SKILL.md 所在目录)。
exec: python ./lib/trigger.py "<老板原始消息文本>"
workdir: <本 skill 目录>
实际调用时,模型应该用 exec 工具的 workdir 参数:
{"command": "python ./lib/trigger.py \"<消息>\"", "workdir": "<skill目录绝对路径>"}
Skill 目录路径可通过 SKILL.md 所在位置确定,不同系统不同:
C:\Users\xxx\openclaw\skills\clawshop-pro/Users/xxx/openclaw/skills/clawshop-pro/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/api.py`),自动加 `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.py`) |
| `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: python ./lib/poll_events.py
**轮询脚本职责(一次 exec 干完):**
1. 调 `events_pending()` 拉所有未处理事件
2. 对每个事件:
- 检查 `processed_events.json`,处理过 → 直接 ack 跳过
- 没处理过 → 按 event_type 分发到对应 send_review_*.py / 处理函数
- 处理成功 → 写入 processed_events + 调 ack
3. 检查是否流程结束(review_2 通过、所有 reject、no_match)→ 停止 cron
4. 输出 JSON 摘要:`{"processed": N, "stopped": true/false}`
### 5.3 子脚本
- `lib/send_review_1.py` — 发一审卡片
- `lib/send_review_1_5.py` — 发分镜卡片
- `lib/send_review_2.py` — 发视频卡片
- `lib/handle_button.py` — 按钮回调
- `lib/card_tools.py` — 卡片工具(卡片工具)
---
## 六、Cron 轮询任务
### 6.1 启动
`lib/trigger.py` 在推荐启动后**注册一个 cron job**:
```python
# 通过 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 轮询事件: 执行 `python ./lib/poll_events.py`,有事件就处理;处理完后回复 NO_REPLY。",
"lightContext": true
},
"deleteAfterRun": false,
"delivery": {"mode": "none"}
}
}
}
关键参数:
everyMs: 5000 — 每 5 秒lightContext: true — 不带历史上下文,省 tokendelivery.mode: "none" — 不发系统消息,由脚本自己发卡片name: clawshop-poll — 固定 ID 方便后续 update/remove触发停止的条件:
review_2 approve / published / reject 全部 / no_match)poll_events.py 内部检查后调:
POST http://127.0.0.1:18789/tools/invoke
Body: {
"tool": "cron",
"args": {"action": "remove", "id": "clawshop-poll"}
}
data/poll_state.json:
{"started_at": 1716180000, "active": true}
每次 poll_events.py 启动时检查 now - started_at > 900 → 强制停止 cron。
收到 Clicked "✅ 做 #4" 等按钮消息:
exec: python ./lib/handle_button.py "<clicked_label>"
脚本内部:
#<short_id> → 查 card_map → 拿 video_id 和 step/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) |
| 文件 | 用途 | 示例 |
|---|---|---|
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/trigger.py |
登录 xxx yyy / login xxx yyy | 触发 lib/trigger.py(脚本内部识别) |
Clicked "..." 按钮回调 | 触发 lib/handle_button.py |
Discord 按钮点击后只给 3 秒响应窗口。handle_button.py 要调 data-service API,经常超过 3 秒。
正确流程(所有按钮都遵守):
收到 Clicked "..." → 立刻回复一句短文本(占住 3s 窗口)→ 然后 exec handle_button.py → NO_REPLY
回复文本规则:
✅ 收到示例:
用户: Clicked "✅ 做 #3"
助手: ⏳ 视频 #3 开始生成分镜图,请稍候...
[exec: python ./lib/handle_button.py "✅ 做 #3"]
NO_REPLY
用户: Clicked "⏭️ 换一个 #5"
助手: ✅ 收到
[exec: python ./lib/handle_button.py "⏭️ 换一个 #5"]
NO_REPLY
绝对禁止: 先调脚本等结果再回复。这会导致 Discord 显示“该交互失败”。
| Cron Clawshop 轮询事件: ... | 触发 lib/poll_events.py |
| 系统提示 Use the "clawshop" skill ... | 按上下文判断走哪个脚本 |
| HEARTBEAT_OK / inter-session announce | NO_REPLY |
/recommend /clawshop/callback,只调 /api/v1/*clawshop 完全独立运行。
config.json 的 data_service.base_url)http://127.0.0.1:18789)NO_REPLY。 回任何其他文字都会被发到 Discord 频道里污染老板的屏幕。| 场景 | 文案 |
|---|---|
| 未登录 | 🔑 还未登录 Clawshop。\n请回复您的账号和密码(中间用空格隔开),例如:\n\myname 123456`` |
| 无 URL | ⏳ 已启动热门推荐,带到合适的视频再给您发审核卡。 |
| 有 URL | ⏳ 指定视频已接收,正在抓取分析,完成后给您发审核卡。 |
| 登录成功 | ✅ 登录成功(用户:{username}),开始推荐... |
| 登录失败 | ❌ 账号密码错误,请确认后重试。 |
| 登录异常 | ❌ 登录失败:{error} |
| token 失效 | ❌ 登录已失效,请重新登录:\登录 用户名 密码`` |
| 按钮 | 进度消息 |
|---|---|
| ✅ 做 #{n} | ⏳ 视频 #{n} 开始生成分镜图,请稍候... |
| ⏭️ 换一个 #{n} | 无(不发进度) |
| 🔄 换商品 #{n} | 🔄 正在重新匹配商品... |
| ✅ 分镜通过 #{n} | ⏳ 分镜已确认,正在生成视频提示词和视频,预计8-10分钟... |
| 🔄 重新生成 #{n} | 🔄 正在重新生成分镜图... |
| ❌ 放弃 #{n} | ❌ 已放弃视频 #{n} |
| ✅ 通过 #{n} | 📤 视频 #{n} 正在提交发布... |
| 🔄 重做 #{n} | 🔄 视频 #{n} 重新生成中... |
| 事件 | 提示文案 |
|---|---|
| review_1 | 发一审卡片(带商品图 + 按钮) |
| review_1_5 正常 | 发分镜卡片(带分镜图 + 按钮) |
| review_1_5 error | ⚠️ 视频 #{n} 分镜生成失败:{error} |
| review_2 正常 | 发成品视频卡片(带视频 + 按钮) |
| review_2 error | ⚠️ 视频 #{n} 生成失败:{error} |
| published | ✅ 视频 #{n} 已成功发布到 {platform}! |
| no_match | 🙅 这轮没找到合适的视频,稍后再试。 |
| error (quota_exhausted) | ⚠️ API余额不足,无法继续生成。请充值后重试。\n错误详情:{error} |
| error (其他) | ⚠️ 服务端异常:{message} |
| HTTP 状态 | 提示文案 |
|---|---|
| 401 | ❌ 登录已失效,请重新登录:\登录 用户名 密码`` |
| 403 | ❌ 操作越权:{error} |
| 404 | ❌ 接口不存在(服务端可能未部署) |
| 5xx | ❌ 服务器错误,请稍后重试 |
| 超时 | ❌ 请求超时 |
| 网络错误 | ❌ 网络连接失败:{error} |