Clawshop Pro

Security

Clawshop 自动带货客户端 skill。装上 → 登录账号密码 → /clawshop 直接用,零配置。

Install

openclaw skills install clawshop-pro

Clawshop Skill

Clawshop 自动带货客户端。装上即用,账号密码登录后所有 AI key、采集系统、视频生成全部由我方服务器统一管理,用户零配置。

clawshop skill 的区别: clawshop 是 SaaS 客户端,走 /api/v1/* + Bearer token + 短轮询事件队列。

⚠️ 铁律

  • 发完审核卡片后绝对不发任何额外消息。 不发"分镜出来了""等老板点按钮""已确认生成中"等。
  • 每个流程最多 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 文本模板(必须用这个,不许改):

  • 未登录:🔑 还未登录 Clawshop。请回复:\登录 用户名 密码``
  • 无 URL:⏳ 已启动热门推荐,带到合适的视频再给您发审核卡。
  • 有 URL:⏳ 指定视频已接收,正在抓取分析,完成后给您发审核卡。
  • 登录成功:✅ 登录成功,开始推荐...
  • 登录失败:❌ 账号密码错误,请确认后重试。

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/*  接口 + 事件队列

二、用户使用流程

2.0 安装后命令自动注册

用户 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

运行时机:

  • skill 首次安装后 → 提示老板跑一次
  • bot 加入新 guild 后 → 重跑一次(幂等,重复跑不出错)

命令:

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 跑一次即可。

2.1 首次(脚本内部自己处理,龙虾只负责调脚本)

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 事件 → 发卡片

2.2 后续

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: 参数)
  • /推荐 命令
  • 文字消息含 URL(如 推荐 https://...
  • 系统提示 Use the "clawshop" skill for this request.
  • 含"登录 xxx yyy"格式的消息(首次登录用)

3.1 URL 提取规则

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 登录指令识别

老板首次登录会发:登录 用户名 密码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)

3.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 所在位置确定,不同系统不同:

  • 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/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 — 不带历史上下文,省 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.py 内部检查后调:

POST http://127.0.0.1:18789/tools/invoke
Body: {
  "tool": "cron",
  "args": {"action": "remove", "id": "clawshop-poll"}
}

6.3 启动时间记录

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>"

脚本内部:

  1. 正则提取 #<short_id> → 查 card_map → 拿 video_id 和 step
  2. 按按钮前缀映射到 action,先发进度消息(不许改)
  3. 调对应的 API(不再是 /clawshop/callback,而是 /api/v1/tasks/.../xxx):
按钮阶段action进度消息(老板可见)API 调用
✅ 做review_1approve⏳ 视频 #{n} 开始生成分镜图,请稍候...task_approve(vid, "review_1")POST /api/v1/tasks/{vid}/approve
⏭️ 换一个review_1reject + recommend无(不发进度)task_reject(vid) + recommend()
🔄 换商品review_1change_product🔄 正在重新匹配商品...task_change_product(vid)
✅ 分镜通过review_1_5approve⏳ 分镜已确认,正在生成视频提示词和视频,预计8-10分钟...task_approve(vid, "review_1_5")POST /api/v1/tasks/{vid}/storyboard/approve
🔄 重新生成review_1_5redo🔄 正在重新生成分镜图...task_storyboard_redo(vid)
❌ 放弃review_1_5/review_2reject❌ 已放弃视频 #{n}task_reject(vid) + 删 card_map + 停 cron
✅ 通过review_2approve📤 视频 #{n} 正在提交发布...task_approve(vid, "review_2")POST /api/v1/tasks/{vid}/video/approve
🔄 重做review_2redo🔄 视频 #{n} 重新生成中...task_video_redo(vid)
  1. 按钮 review_2 approve / reject 全部 → 删 card_map + 停 cron
  2. 按钮回调后继续 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/trigger.py
登录 xxx yyy / login xxx yyy触发 lib/trigger.py(脚本内部识别)
Clicked "..." 按钮回调触发 lib/handle_button.py

9.1 按钮回调必须先回复再执行(铁律)

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 完全独立运行。


十一、依赖

  • data-service(地址在 config.jsondata_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_*.py 发卡片。绝对不允许做“是不是重复”的判断,不允许跳过。 即使同一个 video_id 反复出现(比如换商品后重推 review_1),也是新卡片,必须发。
  10. 按钮回调必须查映射文件确认 video_id,不能猜。 查不到就问老板,不要自己猜。

十三、完整提示文案汇总(锁定,不许改)

斜杠命令 ack

场景文案
未登录🔑 还未登录 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}

API 错误

HTTP 状态提示文案
401❌ 登录已失效,请重新登录:\登录 用户名 密码``
403❌ 操作越权:{error}
404❌ 接口不存在(服务端可能未部署)
5xx❌ 服务器错误,请稍后重试
超时❌ 请求超时
网络错误❌ 网络连接失败:{error}