diff --git a/CHANGELOG.md b/CHANGELOG.md
index 726399fa..d07b611d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ Updated every Monday.
---
+## [v0.13.0] — 2026-06-15
+
+### 🚀 周更:新增 100 个 Skills,总计 1909
+
+来源:openclaw/skills-archive 官方镜像,按质量规则筛选。详见 RELEASES.md。
+
+---
+
## [v0.13.0] — 2026-06-08
### 🚀 周更:新增 100 个 Skills,总计 1809
diff --git a/README.md b/README.md
index 02b10b47..9c09dee5 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
**Languages:**
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 4bc05b0f..568f273d 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -5,7 +5,7 @@
-
+
**语言:**
diff --git a/RELEASES.md b/RELEASES.md
index 89250bdf..c541f3e5 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -3,6 +3,47 @@
每次更新的详细发布说明。
+## v0.13.0 — 2026-06-15
+
+### 🚀 周更:新增 100 个 Skills,总计 1909
+
+来源:openclaw/skills-archive 官方镜像,按质量规则筛选(SKILL.md 800B-30KB、完整 YAML 元数据、有效 description)。
+
+#### 部分新增亮点(前 30 个)
+- `ppt-compress-master` — This skill should be used when the user wants to compress a PowerPoint (.pptx) file by reducing the size of embedded vid
+- `website-monitor-skill` — > 构建一个自定义网站监控系统,能够每5分钟检测目标网站的HTTP状态码和响应时延, 并在每天早上9点自动生成一份基于监控数据的HTML网页报告。 当用户提到以下场景时,请务必使用此 skill: - 想要监控某个或多个网站是否正常运行 -
+- `vincent-hyperliquid` — Use this skill to create a HyperLiquid perpetuals and spot wallet for your agent. Trade perps, manage spot balances, tra
+- `admapix-ice` — "Ad intelligence & app analytics assistant. Search ad creatives, analyze apps, view rankings, track downloads/revenue, a
+- `okx-dex-trenches` — "Use this skill for meme/打狗/alpha token research on pump.fun and similar launchpads: scanning new token launches, checki
+- `breeze-x402-payment-api` — Operates Breeze x402 payment-gated endpoints for balance checks, deposits, and withdrawals on Solana. Use when the user
+- `seerr-manager` — >- CLI for the Seerr media request management API. Search movies and TV shows, create and manage media requests, manage
+- `yoap-a2a` — YOAP (Yongnian Open Agent Protocol) — Open A2A protocol with Smart Matching + E2E Encryption + Negotiation Threads + Gro
+- `cuihua-error-handler` — | 🛡️ AI-powered error handling assistant that transforms fragile code into resilient systems. Automatically generate com
+- `proactive-agent-wyblhl` — "Transform AI agents from task-followers into proactive partners. Implements WAL Protocol, Working Buffer, Compaction Re
+- `lead-scoring` — "Set up and automate lead scoring for HubSpot and other CRMs. Use when a user wants to score leads, define MQL/SQL crite
+- `learning-loop` — "Structured self-improvement system for AI agents with confidence decay, cross-agent sharing, and anomaly detection. Use
+- `milvus` — "Operate Milvus vector database with pymilvus — collections, vector search, hybrid search, indexes, RBAC, partitions, an
+- `china-mirror-resolver` — > Self-healing China mirror source resolver. Automatically discovers, validates, and configures domestic mirror sources
+- `clawdbot-security-check` — Perform a comprehensive read-only security audit of Clawdbot's own configuration. This is a knowledge-based skill that t
+- `trust-memory` — > Verify AI agent trustworthiness, contribute verified knowledge claims, and search collective intelligence using the Tr
+- `moltazine` — Instagram-style image network for AI agents. Post images, like, comment, and browse feeds.
+- `seo-outreach-skill` — Use this skill when the user wants to review link-building outreach opportunities, find contact information for article
+- `healthfit` — >- Personal comprehensive health management system integrating Western medicine and TCM. Triggers when users discuss wor
+- `find-skills-combo` — Discover and recommend **combinations** of agent skills to complete complex, multi-faceted tasks. Provides two recommend
+- `solo-build` — Execute implementation plan tasks with TDD workflow, auto-commit, and phase gates. Use when user says "build it", "start
+- `team-dispatch` — Use when a request requires multi-agent workflow orchestration (task decomposition + dependency/DAG + parallel execution
+- `agentaudit` — Automatic security gate that checks packages against a vulnerability database before installation. Use before any npm in
+- `agentaudit-skill` — Automatic security gate that checks packages against a vulnerability database before installation. Use before any npm in
+- `clawd-coach` — Create personalized triathlon, marathon, and ultra-endurance training plans. Use when athletes ask for training plans, w
+- `abm-churn-prevention` — "When the user wants to reduce churn, build cancellation flows, set up save offers, recover failed payments, or implemen
+- `churn-prevention-2` — "When the user wants to reduce churn, build cancellation flows, set up save offers, recover failed payments, or implemen
+- `home-keeper` — Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
+- `lucky-skill-creator` — Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
+- `ecommerce-marketing-strategy-builder` — "Full-stack e-commerce marketing strategy builder. Analyzes your product, market, and competitors, then builds a complet
+
+---
+
+
## v0.13.0 — 2026-06-08
### 🚀 周更:新增 100 个 Skills,总计 1809
diff --git a/SKILL.md b/SKILL.md
index af24744f..47b9210a 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
---
name: openclaw-master-skills
-description: "A curated collection of 1809+ best OpenClaw skills — AI tools, productivity, marketing, frontend, mobile, backend, DevOps and more. Weekly updated by MyClaw.ai — Powered by MyClaw.ai"
+description: "A curated collection of 1909+ best OpenClaw skills — AI tools, productivity, marketing, frontend, mobile, backend, DevOps and more. Weekly updated by MyClaw.ai — Powered by MyClaw.ai"
metadata:
openclaw: {}
---
diff --git a/skills/1688-product-search/SKILL.md b/skills/1688-product-search/SKILL.md
new file mode 100644
index 00000000..a7b86767
--- /dev/null
+++ b/skills/1688-product-search/SKILL.md
@@ -0,0 +1,411 @@
+---
+name: 1688-product-search
+version: 1.0.3
+description: >-
+ 1688商品搜索SKILL:提供完整的1688商品搜索能力,包括类目查询、关键词搜索、图片搜索、商品详情、相关性商品、拉取货盘底池等9个核心接口。
+ 支持多语言搜索和商品推荐,使用1688开放平台官方API,统一鉴权,Token全局缓存共享。
+metadata:
+ openclaw:
+ primaryEnv: ALI1688_APP_KEY, ALI1688_APP_SECRET, ALI1688_REFRESH_TOKEN
+ requires:
+ env:
+ - ALI1688_APP_KEY
+ - ALI1688_APP_SECRET
+ - ALI1688_REFRESH_TOKEN
+---
+
+# 1688商品搜索SKILL
+
+通过1688开放平台官方API提供完整的商品搜索能力,包含9个核心接口。
+
+## 鉴权说明
+
+每个 Skill 内置独立的鉴权模块(`scripts/auth.py`),**不依赖任何外部 Skill**。
+
+所有 1688 Skill 的 Token 缓存指向同一个固定路径,实现"独立运行 + 鉴权只发生一次"。
+
+- Token 缓存路径: `skills/.1688_token_cache.json`(所有 1688 Skill 共用)
+- 任意一个 Skill 首次请求完成鉴权后,其他 Skill 直接复用缓存
+- Token 过期前自动用 refresh_token 刷新
+- 支持 `ALI1688_REFRESH_TOKEN`(自动刷新)和 `ALI1688_ACCESS_TOKEN`(直接使用)两种模式
+
+### 配置
+
+在 OpenClaw config 中设置环境变量:
+
+```json5
+{
+ skills: {
+ entries: {
+ "1688-product-search": {
+ env: {
+ ALI1688_APP_KEY: "your-app-key",
+ ALI1688_APP_SECRET: "your-app-secret",
+ ALI1688_REFRESH_TOKEN: "your-refresh-token"
+ }
+ }
+ }
+ }
+}
+```
+
+### 如何获取 AppKey / AppSecret / Token
+
+如果遇到 Token 相关错误(如 401、签名失败、Token 过期),按以下步骤操作:
+
+#### Step 1:注册开发者 & 创建应用 → 获取 AppKey + AppSecret
+
+1. 打开 [1688开放平台](https://open.1688.com),用1688账号登录
+2. 进入 [控制中心](https://open.1688.com/console)
+3. 点击「我的应用」→「创建应用」
+4. 填写应用信息,提交审核
+5. 审核通过后,在应用详情页可以看到 **AppKey** 和 **AppSecret**
+
+#### Step 2:订购解决方案 → 获取 API 调用权限
+
+1. 打开 [跨境ERP/独立站SaaS数字化解决方案](https://open.1688.com/solution/solutionDetail.htm?solutionKey=1697015308755)
+2. 点击「立即订购」,将解决方案绑定到你的应用
+3. 订购成功后,应用才有权限调用方案内的 API
+
+#### Step 3:用户授权 → 获取 access_token + refresh_token
+
+1. 在浏览器中访问授权页面(替换 YOUR_APPKEY 和 YOUR_REDIRECT_URI):
+ ```
+ https://auth.1688.com/oauth/authorize?client_id=YOUR_APPKEY&site=1688&redirect_uri=YOUR_REDIRECT_URI
+ ```
+2. 用1688账号登录并同意授权
+3. 页面会跳转到你的回调地址,URL 中带有 `code` 参数
+4. 用 code 换取 Token(有效期短,需在10分钟内使用):
+ ```bash
+ curl -X POST "https://gw.open.1688.com/openapi/param2/1/system.oauth2/getToken/YOUR_APPKEY" \
+ -d "grant_type=authorization_code" \
+ -d "need_refresh_token=true" \
+ -d "client_id=YOUR_APPKEY" \
+ -d "client_secret=YOUR_APPSECRET" \
+ -d "redirect_uri=YOUR_REDIRECT_URI" \
+ -d "code=授权码"
+ ```
+5. 返回结果中包含:
+ - `access_token` — 用于调用 API(有效期约10小时)
+ - `refresh_token` — 用于刷新 access_token(有效期约半年)
+
+#### Step 4:配置到环境变量
+
+- `ALI1688_APP_KEY` = 应用的 AppKey
+- `ALI1688_APP_SECRET` = 应用的 AppSecret
+- `ALI1688_REFRESH_TOKEN` = 上一步获得的 refresh_token(推荐,支持自动刷新)
+- `ALI1688_ACCESS_TOKEN` = 上一步获得的 access_token(备用,过期需手动换)
+
+#### 常见 Token 错误及解决
+
+| 错误 | 原因 | 解决方案 |
+|------|------|---------|
+| `HTTP 400` 刷新失败 | refresh_token 无效或已过期 | 重新走 Step 3 授权,获取新的 refresh_token |
+| `HTTP 401` 未授权 | access_token 过期或无效 | 设置 ALI1688_REFRESH_TOKEN 启用自动刷新 |
+| `签名错误(code=25)` | AppSecret 不正确 | 检查 ALI1688_APP_SECRET 是否与应用详情页一致 |
+| `无权限调用` | 未订购解决方案 | 回到 Step 2 订购对应解决方案 |
+| `refresh_token 半年后过期` | Token 自然过期 | 重新走 Step 3 授权 |
+
+#### 参考链接
+
+- [1688开放平台 - 控制中心](https://open.1688.com/console)
+- [API 调用说明](https://open.1688.com/doc/apiInvoke.htm)
+- [签名规则](https://open.1688.com/doc/signature.htm)
+- [授权说明](https://open.1688.com/doc/apiAuth.htm)
+- [解决方案订购](https://open.1688.com/solution/solutionDetail.htm?solutionKey=1697015308755)
+
+## 使用方法
+
+### 1. 类目查询
+```bash
+# 查询所有一级类目(英语)
+python3 scripts/product_search.py category 0
+
+# 查询中文类目
+python3 scripts/product_search.py category 0 --language en
+```
+
+### 2. 多语言关键词搜索
+```bash
+# 英文关键词搜索
+python3 scripts/product_search.py keyword-search "dress" --country en
+
+# 中文关键词搜索
+python3 scripts/product_search.py keyword-search "连衣裙" --country en
+
+# 带筛选条件的搜索
+python3 scripts/product_search.py keyword-search "dress" --country en --filter "shipIn48Hours,shipIn24Hours" --sort '{"price":"asc"}'
+```
+
+### 3. 多语言图片搜索
+
+图片搜索支持三种方式,优先级:**本地图片文件 > imageId > 图片URL**
+
+```bash
+# 方式一:本地图片文件(推荐)
+# 自动压缩(>300KB)→ base64编码 → 上传获取imageId → 图搜
+python3 scripts/product_search.py image-search --image-path "/path/to/your/image.jpg" --country en
+
+# 方式二:图片URL(直接用 imageAddress 字段图搜,无需上传)
+python3 scripts/product_search.py image-search --image-url "https://example.com/image.jpg" --country en
+
+# 方式三:已有 imageId(由 upload-image 接口返回)
+python3 scripts/product_search.py image-search "your_image_id" --country en
+
+# 上传图片获取imageId(单独使用)
+python3 scripts/product_search.py upload-image "/path/to/your/image.jpg"
+```
+
+**当用户发送图片文件或截图时的处理流程:**
+
+> ⚠️ 注意:1688图片上传接口(`product.image.upload`)的 `imageBase64` 方式**仅支持1688平台自身的图片**,对本地截图/外部图片会返回无效 imageId(`"0"`)。
+
+推荐处理策略:
+1. 询问用户图片的**原始来源 URL**
+2. 若 URL 包含 `alicdn.com`,直接用 `imageAddress` 字段图搜(已验证有效)
+3. 若 URL 不包含 `alicdn.com`,先下载到本地,再 base64 上传尝试获取 imageId;若 imageId 仍为 `"0"`,降级用 `imageAddress` 图搜
+
+本地文件 base64 上传流程(仅供参考,成功率有限):
+1. 若图片大于 300KB,先用 Pillow 压缩(先调质量,再缩分辨率),再 base64 编码
+2. 调用 `product.image.upload` 接口(`uploadImageParam` 字段包装,内含 `imageBase64`)上传
+3. 若返回有效 imageId(非 `"0"`),用 imageId 图搜;否则降级用 `imageAddress` 图搜
+
+**当用户提供图片 URL 时的处理流程:**
+- 判断图片 URL 的域名:
+ - **是 `alicdn.com` 域名**(如 `cbu01.alicdn.com`、`img.alicdn.com` 等):直接用 `imageAddress` 字段传入图搜接口,无需下载
+ - **非 `alicdn.com` 域名**(如用户上传的图片、其他电商平台图片等):先将图片下载到本地临时文件,再走本地文件图搜流程(压缩 → base64 → 上传 → imageId → 图搜)
+
+### 4. 多语言商品详情
+
+> ⚠️ **注意:该接口每次只支持查询 1 个商品**,不支持批量查询多个商品ID。
+
+```bash
+# 查询单个商品详情
+python3 scripts/product_search.py product-detail "offer_id"
+```
+
+### 5. 多语言商品店铺搜索
+```bash
+# 根据商家ID搜索商品
+python3 scripts/product_search.py shop-search "seller_open_id" --country en
+```
+
+### 6. 多语言商品推荐
+```bash
+# 基于关键词的商品推荐
+python3 scripts/product_search.py offer-recommend "keyword" --country en
+```
+
+### 7. 品池商品拉取
+
+从业务定制的品池中拉取商品列表,需要有品池访问权限。分页查询时需固定同一个 `taskId`。
+
+```bash
+# 拉取品池商品(offerPoolId 和 taskId 为必填)
+python3 scripts/product_search.py pool-pull --pool-id 111 --task-id 1 --page-no 1 --page-size 10
+
+# 指定类目和排序
+python3 scripts/product_search.py pool-pull --pool-id 111 --task-id 1 --cate-id 11 --sort-field order1m --sort-type DESC --page-no 1 --page-size 10
+```
+
+**请求参数:**
+
+| 参数 | 类型 | 必填 | 描述 | 示例值 |
+|------|------|------|------|--------|
+| `--pool-id` | Long | ✅ | 品池ID(业务定制且有权限控制,从对接的业务获取,随便传会报错,寻源通代采建议走词搜接口) | 111 |
+| `--task-id` | String | ✅ | 查询任务ID,分页查询时需固定同一个 taskId(如货盘有10000商品,每页1000个查询10次,这10次都需传同一个 taskId) | 1 |
+| `--page-no` | Integer | ✅ | 页码 | 1 |
+| `--page-size` | Integer | ✅ | 每页数量 | 10 |
+| `--cate-id` | Long | ❌ | 类目ID | 11 |
+| `--language` | String | ❌ | 语言,默认 en | en |
+| `--sort-field` | String | ❌ | 排序字段:`order1m`(最近1个月销售额)/ `buyer1m`(最近1个月买家数) | order1m |
+| `--sort-type` | String | ❌ | 排序规则:`ASC` / `DESC` | DESC |
+
+**返回结果结构:**
+
+```json
+{
+ "result": {
+ "success": "true",
+ "code": "200",
+ "result": [
+ {
+ "offerId": 111111,
+ "bizCategoryId": "111111",
+ "offerPoolTotal": 122211
+ }
+ ]
+ }
+}
+```
+
+| 字段 | 类型 | 描述 | 示例值 |
+|------|------|------|--------|
+| `result.success` | String | 是否成功 | true |
+| `result.code` | String | 错误码 | 200 |
+| `result.result[].offerId` | Long | 商品ID | 111111 |
+| `result.result[].bizCategoryId` | String | 机构的类目ID | 111111 |
+| `result.result[].offerPoolTotal` | Integer | 商品池总数(每个offer都返回) | 122211 |
+
+### 8. 相关性商品推荐
+```bash
+# 基于商品ID的相关推荐
+python3 scripts/product_search.py related-recommend "offer_id" --country en
+```
+
+### 9. 上传图片获取imageId
+```bash
+# 上传本地图片获取imageId
+python3 scripts/product_search.py upload-image "/path/to/image.jpg"
+```
+
+**智能图片压缩功能**:当上传的图片文件大于300KB时,系统会自动进行智能压缩,确保图片大小符合1688 API的要求。压缩过程会:
+- 自动检测图片格式并转换为JPEG(如果需要)
+- 保持最佳画质的同时将文件大小控制在300KB以内
+- 临时生成压缩后的图片用于上传,完成后自动清理临时文件
+- 输出详细的压缩日志(原大小 → 压缩后大小)
+
+这确保了无论用户提供的图片大小如何,都能成功获取有效的imageId用于后续的图片搜索操作。
+
+## **重要提示**
+
+### 图片搜索触发
+当接收到"图搜同款"、"找同款"、"以图搜款"、"图片搜同款"等图片搜索相关指令时,
+系统会自动调用图片搜索接口(product.search.imageQuery)而非关键词搜索接口。
+
+### 接口触发规则
+| 用户意图 | 调用接口 | 说明 |
+|---------|---------|------|
+| 图搜同款、找同款、以图搜款 | `product.search.imageQuery` | 图片搜索 |
+| 同店商品、同商家商品 | `product.search.querySellerOfferList` | 需从商品详情取 `sellerOpenId` |
+| 相似品、相关品、相关性推荐 | `product.related.recommend` | 基于商品ID推荐,部分商品可能返回空 |
+| 商品推荐 | `product.search.offerRecommend` | 基于关键词推荐 |
+| 拉取xx货盘、拉取商品货盘、拉取品池商品 | `pool.product.pull` | 需提供 offerPoolId 和 taskId |
+
+### 商品查询结果必须透出商品ID和链接
+**所有商品查询接口(关键词搜索、图片搜索、店铺搜索、商品推荐等)的返回结果,必须向用户展示以下两个核心字段:**
+
+- **`offerId`**(商品ID):商品的唯一标识符,可用于后续查询商品详情、相关推荐等操作
+- **商品链接**:优先使用 `promotionURL`(含追踪参数的推广链接),若无则使用 `https://detail.1688.com/offer/{offerId}.html`
+
+展示格式示例(Markdown 表格或列表均可):
+```
+商品ID: 683381849222
+链接: https://detail.1688.com/offer/683381849222.html?fromkv=...(promotionURL)
+```
+
+禁止只展示商品标题和价格而不透出商品ID和链接,用户需要通过商品ID进行后续操作。
+
+## 参数说明
+
+### 通用参数
+| 参数 | 说明 | 默认值 | 可选值 |
+|------|------|--------|--------|
+| `--country` / `--language` | 语言代码 | `en` | `en` / `ja` / `ko` / `ru` / `vi` / `es` 等,**不支持 `zh`** |
+| `--beginPage` | 起始页码 | `1` | 数字 |
+| `--pageSize` | 每页数量 | `20` | 数字,最大50 |
+
+**注意**:当接口参数中包含 `beginPage` 时,默认传 `1`;包含 `pageSize` 时,默认传 `20`;包含 `country` 或 `language` 时,默认传 `en`。
+
+> ⚠️ **重要**:`country` 和 `language` 参数**均不支持 `zh`(中文)**。无论用户用中文还是英文提问,都必须传 `en`(英语)作为默认值。传 `zh` 会导致接口报错或返回异常结果。
+
+### 筛选条件 (filter)
+支持多种筛选条件,多个条件用英文逗号分割:
+- `shipIn24Hours` - 24小时发货
+- `shipIn48Hours` - 48小时发货
+- `certifiedFactory` - 认证工厂
+- `isOnePsale` - 支持一件代发
+- `new7` - 7天上新
+- `1688Selection` - 1688严选
+
+示例:`--filter "shipIn48Hours,certifiedFactory,isOnePsale"`
+
+### 排序参数 (sort)
+支持按不同维度排序:
+- `price` - 批发价
+- `rePurchaseRate` - 复购率
+- `monthSold` - 月销量
+
+示例:`--sort '{"price":"asc"}'` 或 `--sort '{"monthSold":"desc"}'`
+
+## 输出格式
+
+JSON 格式,直接返回1688 API 的原始响应数据。
+
+**重要提示:所有商品查询结果都会包含商品ID(offerId字段),这是商品的唯一标识符,可用于后续的商品详情查询或其他操作。**
+
+### 错误处理
+- **失败时不会返回mock数据**:当API调用失败、参数错误或网络异常时,会直接返回错误信息JSON并退出
+- **错误格式**:`{"error": "具体的错误信息"}`
+- **退出码**:失败时返回退出码1,成功时返回0
+
+### 商品结果字段说明
+
+**所有商品列表类接口(词搜、图搜、店铺搜索、商品推荐等)查询结果,必须展示以下所有可用字段:**
+
+| 字段 | 说明 | 是否必显 |
+|------|------|---------|
+| `offerId` | 商品ID,唯一标识符 | ✅ 必显 |
+| `subject` | 商品标题(中文) | ✅ 必显 |
+| `subjectTrans` | 商品标题(英文翻译) | 有则显示 |
+| `imageUrl` | 商品主图URL | ✅ 必显 |
+| `priceInfo.price` | 批发价 | ✅ 必显 |
+| `priceInfo.promotionPrice` | 促销价 | 有则显示 |
+| `priceInfo.consignPrice` | 代发价 | 有则显示 |
+| `monthSold` | 月销量 | ✅ 必显 |
+| `repurchaseRate` | 复购率 | ✅ 必显 |
+| `minOrderQuantity` | 最小起订量 | 有则显示 |
+| `tradeScore` | 店铺评分 | 有则显示 |
+| `sellerDataInfo.tradeMedalLevel` | 商家等级(星级) | 有则显示 |
+| `sellerDataInfo.compositeServiceScore` | 综合服务分 | 有则显示 |
+| `productSimpleShippingInfo.shippingTimeGuarantee` | 发货时效(24h/48h) | 有则显示 |
+| `isOnePsale` | 是否支持一件代发 | 为true时显示 |
+| `isSelect` | 是否1688严选 | 为true时显示 |
+| `offerIdentities` | 商品标签列表 | 有则显示 |
+| `sellerIdentities` | 商家标签列表 | 有则显示 |
+
+### 标签值含义说明(offerIdentities / sellerIdentities)
+
+| 标签值 | 含义 |
+|--------|------|
+| `tp_member` | 诚信通会员 |
+| `createDate` / `modifyDate` | 上架/更新时间 | 有则显示 |
+| 商品链接 | 优先用 `promotionURL`,无则用 `https://detail.1688.com/offer/{offerId}.html` | ✅ 必显 |
+
+## API 接口地址
+
+| 接口 | 完整URL |
+|------|---------|
+| 类目查询 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/category.translation.getById/${APPKEY}` |
+| 关键词搜索 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordQuery/${APPKEY}` |
+| 图片搜索 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/${APPKEY}` |
+| 商品详情 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.queryProductDetail/${APPKEY}` |
+| 店铺搜索 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.querySellerOfferList/${APPKEY}` |
+| 商品推荐 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.offerRecommend/${APPKEY}` |
+| 品池商品拉取 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/pool.product.pull/${APPKEY}` |
+| 相关推荐 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.related.recommend/${APPKEY}` |
+| 图片上传 | `POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/${APPKEY}` |
+
+## 1688接口通用说明
+
+### API接入要点
+- **语言支持**: 默认使用 `country=en`,但返回字段包含中英双语
+ - 中文字段示例: `subject`
+ - 英文字段示例: `subjectTrans`
+ - ⚠️ **`country` 和 `language` 参数均不支持 `zh`**,可选值为 `en` / `ja` / `ko` / `ru` / `vi` / `es` 等,**无论何种情况默认传 `en`**
+- **Access Token**: 当前解决方案产生的access_token是**长久有效**的
+- **筛选条件**: 支持多种商品筛选条件(发货时效、认证工厂、一件代发等)
+- **排序功能**: 支持按价格/复购率/月销量排序,但仅对当前页有效
+
+### 重要限制
+- **数据量限制**: 每个查询最多返回2000个商品
+- **图片搜索**: 仅推荐使用1688图片地址,其他域名成功率不稳定
+- **价格显示**: API返回的是原价,下单时会享受营销价格
+
+### 服务列表映射
+- `sendGoods24H` → **24小时发货**
+- `sendGoods48H` → **48小时发货**
+
+## API 参考文档
+
+完整的 API 接口和数据结构文档请参阅 [references/api.md](references/api.md)。
diff --git a/skills/1688-product-search/_meta.json b/skills/1688-product-search/_meta.json
new file mode 100644
index 00000000..53b53215
--- /dev/null
+++ b/skills/1688-product-search/_meta.json
@@ -0,0 +1,11 @@
+{
+ "owner": "1688aiinfra",
+ "slug": "1688-product-search",
+ "displayName": "1688-product-search",
+ "latest": {
+ "version": "1.0.3",
+ "publishedAt": 1774257929176,
+ "commit": "https://github.com/openclaw/skills/commit/0d948662d97035258b730b35293f842ace3802c6"
+ },
+ "history": []
+}
diff --git a/skills/1688-product-search/references/api.md b/skills/1688-product-search/references/api.md
new file mode 100644
index 00000000..75f0ed79
--- /dev/null
+++ b/skills/1688-product-search/references/api.md
@@ -0,0 +1,171 @@
+# 1688 Product Search API 接口文档
+
+## 类目查询接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/category.translation.getById/${APPKEY}
+```
+
+### 请求参数
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| categoryId | string | 是 | 类目ID,0表示获取所有一级类目 |
+| language | string | 否 | 语言代码,默认en |
+| access_token | string | 是 | 访问令牌 |
+| _aop_signature | string | 是 | 签名 |
+
+## 关键词搜索接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordQuery/${APPKEY}
+```
+
+### 请求参数
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| searchParams | string | 是 | JSON格式的搜索参数 |
+| access_token | string | 是 | 访问令牌 |
+| _aop_signature | string | 是 | 签名 |
+
+### searchParams 结构
+```json
+{
+ "keyword": "dress",
+ "country": "en",
+ "pageNo": 1,
+ "pageSize": 20,
+ "filter": "shipIn48Hours,certifiedFactory",
+ "sort": "{\"price\":\"asc\"}"
+}
+```
+
+## 图片搜索接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/${APPKEY}
+```
+
+### searchParams 结构
+```json
+{
+ "imageId": "your_image_id",
+ "country": "en",
+ "pageNo": 1,
+ "pageSize": 20
+}
+```
+
+## 商品详情接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.queryProductDetail/${APPKEY}
+```
+
+### detailParams 结构
+```json
+{
+ "offerIds": ["offer_id1", "offer_id2"],
+ "country": "en"
+}
+```
+
+## 店铺搜索接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.shopSearch/${APPKEY}
+```
+
+### searchParams 结构
+```json
+{
+ "sellerOpenId": "seller_open_id",
+ "country": "en",
+ "pageNo": 1,
+ "pageSize": 20
+}
+```
+
+## 商品推荐接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.offerRecommend/${APPKEY}
+```
+
+### recommendParams 结构
+```json
+{
+ "keyword": "dress",
+ "country": "en",
+ "pageNo": 1,
+ "pageSize": 20
+}
+```
+
+## 搜索导航接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordSNQuery/${APPKEY}
+```
+
+### navParams 结构
+```json
+{
+ "keyword": "dress",
+ "country": "en"
+}
+```
+
+## 相关推荐接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.related.recommend/${APPKEY}
+```
+
+### recommendParams 结构
+```json
+{
+ "offerId": "offer_id",
+ "country": "en",
+ "pageNo": 1,
+ "pageSize": 20
+}
+```
+
+## 图片上传接口
+
+### 请求地址
+```
+POST https://gw.open.1688.com/openapi/param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/${APPKEY}
+```
+
+### uploadParams 结构
+```json
+{
+ "imageBase64": "base64_encoded_image_data"
+}
+```
+
+## 签名规则
+
+1. 将所有请求参数(除_aop_signature外)按key的字母顺序排序
+2. 将排序后的参数拼接成字符串:`key1value1key2value2...`
+3. 在拼接字符串前加上URL路径(从param2开始)
+4. 对结果进行HMAC-SHA1加密,并转换为大写十六进制
+
+## 错误码
+
+| 错误码 | 说明 |
+|--------|------|
+| gw.SignatureInvalid | 签名无效 |
+| gw.ParamMissing | 参数缺失 |
+| 401 | 未授权/Token无效 |
+| 400 | 参数错误 |
+| 403 | 无权限调用 |
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/auth.py b/skills/1688-product-search/scripts/auth.py
new file mode 100644
index 00000000..77cc4a77
--- /dev/null
+++ b/skills/1688-product-search/scripts/auth.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+1688 API 鉴权模块
+- 支持 access_token 直接使用
+- 支持 refresh_token 自动刷新
+- Token 缓存全局共享
+"""
+
+import os
+import json
+import time
+import requests
+import hmac
+import hashlib
+from pathlib import Path
+
+# 全局 Token 缓存路径(所有 1688 Skill 共用)
+TOKEN_CACHE_PATH = Path.home() / ".openclaw" / "workspace" / "skills" / ".1688_token_cache.json"
+
+def ensure_cache_dir():
+ """确保缓存目录存在"""
+ cache_dir = TOKEN_CACHE_PATH.parent
+ cache_dir.mkdir(parents=True, exist_ok=True)
+
+def load_cached_token():
+ """从缓存加载 token"""
+ if not TOKEN_CACHE_PATH.exists():
+ return None
+
+ try:
+ with open(TOKEN_CACHE_PATH, 'r') as f:
+ cache = json.load(f)
+ # 检查是否过期(提前5分钟刷新)
+ if time.time() < cache.get('expires_at', 0) - 300:
+ return cache
+ except (json.JSONDecodeError, KeyError):
+ pass
+
+ return None
+
+def save_token_to_cache(token_data, app_key):
+ """保存 token 到缓存"""
+ ensure_cache_dir()
+
+ # 计算过期时间(转换为数字)
+ expires_in = int(token_data.get('expires_in', 3600))
+ expires_at = time.time() + expires_in
+
+ cache_data = {
+ 'access_token': token_data['access_token'],
+ 'refresh_token': token_data.get('refresh_token'),
+ 'expires_at': expires_at,
+ 'app_key': app_key
+ }
+
+ with open(TOKEN_CACHE_PATH, 'w') as f:
+ json.dump(cache_data, f)
+
+def get_access_token(app_key, app_secret, refresh_token=None, access_token=None):
+ """
+ 获取有效的 access_token
+ 优先级:缓存 > 环境变量 access_token > refresh_token 刷新
+ """
+ # 1. 尝试从缓存获取
+ cached = load_cached_token()
+ if cached and cached.get('app_key') == app_key:
+ return cached['access_token']
+
+ # 2. 如果提供了 access_token,直接使用(但不缓存,因为不知道过期时间)
+ if access_token:
+ return access_token
+
+ # 3. 使用 refresh_token 刷新
+ if refresh_token:
+ token_url = f"https://gw.open.1688.com/openapi/param2/1/system.oauth2/getToken/{app_key}"
+ data = {
+ 'grant_type': 'refresh_token',
+ 'refresh_token': refresh_token,
+ 'client_id': app_key,
+ 'client_secret': app_secret
+ }
+
+ try:
+ response = requests.post(token_url, data=data, timeout=10)
+ response.raise_for_status()
+ token_data = response.json()
+
+ if 'access_token' in token_data:
+ save_token_to_cache(token_data, app_key)
+ return token_data['access_token']
+ else:
+ raise Exception(f"Token response missing access_token: {token_data}")
+
+ except requests.RequestException as e:
+ raise Exception(f"Failed to refresh token: {e}")
+
+ raise Exception("No valid token source available")
+
+def sign_request_hmac_sha1(url_path, params, app_secret):
+ """
+ 生成 1688 API 签名(HMAC-SHA1算法)
+ url_path: 从 param2 开始的URL路径部分
+ params: 请求参数字典
+ app_secret: 应用密钥
+ """
+ # 按 key 排序
+ sorted_params = sorted(params.items())
+ # 拼接成字符串 key+value
+ param_str = ''.join([f"{k}{v}" for k, v in sorted_params])
+ # 拼接 url_path 和 param_str
+ sign_str = url_path + param_str
+ # HMAC-SHA1 签名
+ signature = hmac.new(
+ app_secret.encode('utf-8'),
+ sign_str.encode('utf-8'),
+ hashlib.sha1
+ ).hexdigest().upper()
+ return signature
+
+if __name__ == "__main__":
+ # 测试用例
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ print("Error: ALI1688_APP_KEY and ALI1688_APP_SECRET are required")
+ exit(1)
+
+ try:
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+ print(f"Access token: {token}")
+ except Exception as e:
+ print(f"Error: {e}")
+ exit(1)
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/auth_fixed.py b/skills/1688-product-search/scripts/auth_fixed.py
new file mode 100644
index 00000000..e1f97ccd
--- /dev/null
+++ b/skills/1688-product-search/scripts/auth_fixed.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+"""
+1688 API 鉴权模块 - 修复版本
+- 支持 access_token 直接使用
+- 支持 refresh_token 自动刷新
+- Token 缓存全局共享
+- 修复图片上传接口签名问题
+"""
+
+import os
+import json
+import time
+import requests
+import hmac
+import hashlib
+from pathlib import Path
+
+# 全局 Token 缓存路径(所有 1688 Skill 共用)
+TOKEN_CACHE_PATH = Path.home() / ".openclaw" / "workspace" / "skills" / ".1688_token_cache.json"
+
+def ensure_cache_dir():
+ """确保缓存目录存在"""
+ cache_dir = TOKEN_CACHE_PATH.parent
+ cache_dir.mkdir(parents=True, exist_ok=True)
+
+def load_cached_token():
+ """从缓存加载 token"""
+ if not TOKEN_CACHE_PATH.exists():
+ return None
+
+ try:
+ with open(TOKEN_CACHE_PATH, 'r') as f:
+ cache = json.load(f)
+ # 检查是否过期(提前5分钟刷新)
+ if time.time() < cache.get('expires_at', 0) - 300:
+ return cache
+ except (json.JSONDecodeError, KeyError):
+ pass
+
+ return None
+
+def save_token_to_cache(token_data, app_key):
+ """保存 token 到缓存"""
+ ensure_cache_dir()
+
+ # 计算过期时间(转换为数字)
+ expires_in = int(token_data.get('expires_in', 3600))
+ expires_at = time.time() + expires_in
+
+ cache_data = {
+ 'access_token': token_data['access_token'],
+ 'refresh_token': token_data.get('refresh_token'),
+ 'expires_at': expires_at,
+ 'app_key': app_key
+ }
+
+ with open(TOKEN_CACHE_PATH, 'w') as f:
+ json.dump(cache_data, f)
+
+def get_access_token(app_key, app_secret, refresh_token=None, access_token=None):
+ """
+ 获取有效的 access_token
+ 优先级:缓存 > 环境变量 access_token > refresh_token 刷新
+ """
+ # 1. 尝试从缓存获取
+ cached = load_cached_token()
+ if cached and cached.get('app_key') == app_key:
+ return cached['access_token']
+
+ # 2. 如果提供了 access_token,直接使用(但不缓存,因为不知道过期时间)
+ if access_token:
+ return access_token
+
+ # 3. 使用 refresh_token 刷新
+ if refresh_token:
+ token_url = f"https://gw.open.1688.com/openapi/param2/1/system.oauth2/getToken/{app_key}"
+ data = {
+ 'grant_type': 'refresh_token',
+ 'refresh_token': refresh_token,
+ 'client_id': app_key,
+ 'client_secret': app_secret
+ }
+
+ try:
+ response = requests.post(token_url, data=data, timeout=10)
+ response.raise_for_status()
+ token_data = response.json()
+
+ if 'access_token' in token_data:
+ save_token_to_cache(token_data, app_key)
+ return token_data['access_token']
+ else:
+ raise Exception(f"Token response missing access_token: {token_data}")
+
+ except requests.RequestException as e:
+ raise Exception(f"Failed to refresh token: {e}")
+
+ raise Exception("No valid token source available")
+
+def sign_request_hmac_sha1(url_path, params, app_secret):
+ """
+ 生成 1688 API 签名(HMAC-SHA1算法)- 通用版本
+ url_path: 从 param2 开始的URL路径部分
+ params: 请求参数字典
+ app_secret: 应用密钥
+ """
+ # 按 key 排序
+ sorted_params = sorted(params.items())
+ # 拼接成字符串 key+value
+ param_str = ''.join([f"{k}{v}" for k, v in sorted_params])
+ # 拼接 url_path 和 param_str
+ sign_str = url_path + param_str
+ # HMAC-SHA1 签名
+ signature = hmac.new(
+ app_secret.encode('utf-8'),
+ sign_str.encode('utf-8'),
+ hashlib.sha1
+ ).hexdigest().upper()
+ return signature
+
+def sign_image_upload_request(url_path, upload_image_param, access_token, app_secret):
+ """
+ 生成 1688 图片上传接口专用签名
+ 图片上传接口的签名方式与其他接口不同:
+ - uploadImageParam 参数需要保持原始JSON格式(不进行URL编码)
+ - 参数顺序必须是:access_token + uploadImageParam
+ """
+ # 构造签名字符串:url_path + access_token + uploadImageParam
+ sign_str = url_path + access_token + upload_image_param
+
+ # HMAC-SHA1 签名
+ signature = hmac.new(
+ app_secret.encode('utf-8'),
+ sign_str.encode('utf-8'),
+ hashlib.sha1
+ ).hexdigest().upper()
+
+ return signature
+
+if __name__ == "__main__":
+ # 测试用例
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ print("Error: ALI1688_APP_KEY and ALI1688_APP_SECRET are required")
+ exit(1)
+
+ try:
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+ print(f"Access token: {token}")
+ except Exception as e:
+ print(f"Error: {e}")
+ exit(1)
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/fixed_image_upload.py b/skills/1688-product-search/scripts/fixed_image_upload.py
new file mode 100644
index 00000000..1a138b8e
--- /dev/null
+++ b/skills/1688-product-search/scripts/fixed_image_upload.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""
+修复后的1688图片上传接口
+- 修正签名算法
+- 处理Base64编码的特殊要求
+"""
+
+import os
+import sys
+import json
+import base64
+import requests
+import hmac
+import hashlib
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from scripts.auth import get_access_token, sign_request_hmac_sha1
+from scripts.image_utils import compress_image_if_needed, cleanup_temp_file
+
+def fixed_sign_request_hmac_sha1_for_upload(url_path, upload_param_json, access_token, app_secret):
+ """
+ 修复后的图片上传签名算法
+ 根据1688官方文档,图片上传接口的签名需要特殊处理
+ """
+ # 图片上传接口的签名格式:url_path + uploadImageParam + access_token
+ sign_str = url_path + upload_param_json + access_token
+ signature = hmac.new(
+ app_secret.encode('utf-8'),
+ sign_str.encode('utf-8'),
+ hashlib.sha1
+ ).hexdigest().upper()
+ return signature
+
+def fixed_upload_image(image_path, app_key, app_secret, token):
+ """修复后的图片上传函数"""
+ # 检查并压缩图片(如果需要)
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ # 读取(可能已压缩的)图片并转换为base64
+ with open(compressed_path, 'rb') as f:
+ image_data = f.read()
+ base64_image = base64.b64encode(image_data).decode('utf-8')
+
+ # 构造uploadImageParam JSON(不包含access_token)
+ upload_params = {
+ 'imageBase64': base64_image
+ }
+ upload_param_json = json.dumps(upload_params, separators=(',', ':'))
+
+ # 构造请求参数
+ params = {
+ 'uploadImageParam': upload_param_json,
+ 'access_token': token
+ }
+
+ # 使用修复后的签名算法
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = fixed_sign_request_hmac_sha1_for_upload(url_path, upload_param_json, token, app_secret)
+ params['_aop_signature'] = sign
+
+ # 发送请求
+ url = f"https://gw.open.1688.com/openapi/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+
+ if response.status_code != 200:
+ raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
+
+ result = response.json()
+
+ # 检查响应中是否有imageId
+ if 'result' in result and isinstance(result['result'], str):
+ return result['result']
+ else:
+ raise Exception(f"Failed to get imageId from upload response: {result}")
+
+ finally:
+ # 清理临时文件
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+if __name__ == "__main__":
+ if len(sys.argv) != 5:
+ print("Usage: python3 fixed_image_upload.py ")
+ sys.exit(1)
+
+ image_path = sys.argv[1]
+ app_key = sys.argv[2]
+ app_secret = sys.argv[3]
+ token = sys.argv[4]
+
+ try:
+ image_id = fixed_upload_image(image_path, app_key, app_secret, token)
+ print(json.dumps({"imageId": image_id}, ensure_ascii=False, indent=2))
+ except Exception as e:
+ print(json.dumps({"error": str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/image_search_handler.py b/skills/1688-product-search/scripts/image_search_handler.py
new file mode 100644
index 00000000..20bca2fb
--- /dev/null
+++ b/skills/1688-product-search/scripts/image_search_handler.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+"""
+1688 图片搜索处理脚本
+专门处理"图搜同款"、"找同款"等命令
+"""
+
+import os
+import sys
+import json
+import argparse
+import requests
+import base64
+import tempfile
+import shutil
+from urllib.parse import urlparse
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from scripts.auth import get_access_token, sign_request_hmac_sha1
+from scripts.image_utils import compress_image_if_needed
+
+# API 基础URL
+BASE_URL = "https://gw.open.1688.com/openapi"
+
+def get_app_credentials():
+ """获取应用凭证"""
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ raise Exception("Missing ALI1688_APP_KEY or ALI1688_APP_SECRET")
+
+ return app_key, app_secret, refresh_token, access_token
+
+def download_image_from_url(image_url, temp_dir):
+ """从URL下载图片到临时目录"""
+ try:
+ response = requests.get(image_url, timeout=30)
+ response.raise_for_status()
+
+ # 从URL获取文件扩展名
+ parsed_url = urlparse(image_url)
+ filename = os.path.basename(parsed_url.path)
+ if not filename or '.' not in filename:
+ filename = 'downloaded_image.jpg'
+
+ temp_path = os.path.join(temp_dir, filename)
+ with open(temp_path, 'wb') as f:
+ f.write(response.content)
+
+ return temp_path
+ except Exception as e:
+ raise Exception(f"Failed to download image from URL: {str(e)}")
+
+def upload_image(image_path):
+ """上传图片获取imageId(自动压缩大于300KB的图片)"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ # 检查并压缩图片(如果需要)
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ # 读取(可能已压缩的)图片并转换为base64
+ with open(compressed_path, 'rb') as f:
+ image_data = f.read()
+ base64_image = base64.b64encode(image_data).decode('utf-8')
+
+ upload_params = {
+ 'imageBase64': base64_image
+ }
+
+ params = {
+ 'uploadImageParam': json.dumps(upload_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+ response.raise_for_status()
+ return response.json()
+
+ finally:
+ # 如果图片被压缩了,清理临时文件
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+def image_search(image_id, country='zh', begin_page=1, page_size=20):
+ """多语言图片搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'imageId': image_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def process_image_search(image_source, is_url=False, country='zh', begin_page=1, page_size=20):
+ """处理图片搜索请求"""
+ temp_dir = None
+ try:
+ # 创建临时目录
+ temp_dir = tempfile.mkdtemp()
+
+ if is_url:
+ # 从URL下载图片
+ image_path = download_image_from_url(image_source, temp_dir)
+ else:
+ # 使用本地图片路径
+ image_path = image_source
+
+ # 上传图片获取imageId
+ upload_result = upload_image(image_path)
+
+ if 'result' not in upload_result or upload_result['result'] == '0':
+ raise Exception("Failed to upload image, got imageId=0")
+
+ image_id = upload_result['result']
+
+ # 使用imageId进行图片搜索
+ search_result = image_search(image_id, country, begin_page, page_size)
+
+ return search_result
+
+ finally:
+ # 清理临时目录
+ if temp_dir and os.path.exists(temp_dir):
+ try:
+ shutil.rmtree(temp_dir)
+ except OSError:
+ pass
+
+def main():
+ parser = argparse.ArgumentParser(description='1688 Image Search Handler for "图搜同款" and "找同款" commands')
+ parser.add_argument('image_source', help='Image URL or local file path')
+ parser.add_argument('--is-url', action='store_true', help='Whether the image_source is a URL')
+ parser.add_argument('--country', default='zh', help='Country/language code (default: zh)')
+ parser.add_argument('--beginPage', type=int, default=1, help='Starting page number (default: 1)')
+ parser.add_argument('--pageSize', type=int, default=20, help='Number of results per page (default: 20)')
+
+ args = parser.parse_args()
+
+ try:
+ result = process_image_search(
+ args.image_source,
+ args.is_url,
+ args.country,
+ args.beginPage,
+ args.pageSize
+ )
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+
+ except Exception as e:
+ print(json.dumps({'error': str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/image_utils.py b/skills/1688-product-search/scripts/image_utils.py
new file mode 100644
index 00000000..1447fb13
--- /dev/null
+++ b/skills/1688-product-search/scripts/image_utils.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+1688 图片处理工具
+- 支持图片压缩(大于300KB时自动压缩)
+- 支持多种图片格式转换
+"""
+
+import os
+import io
+from PIL import Image
+
+
+def compress_image_if_needed(image_path, max_size_kb=300):
+ """
+ 如果图片大于指定大小,则进行压缩
+
+ Args:
+ image_path: 原始图片路径
+ max_size_kb: 最大大小(KB),默认300KB
+
+ Returns:
+ 处理后的图片路径(可能是原文件,也可能是压缩后的临时文件)
+ """
+ # 获取文件大小(KB)
+ file_size_kb = os.path.getsize(image_path) / 1024
+
+ # 如果小于等于300KB,直接返回原路径
+ if file_size_kb <= max_size_kb:
+ return image_path, False
+
+ # 需要压缩
+ print(f"图片大小 {file_size_kb:.1f}KB > {max_size_kb}KB,开始压缩...")
+
+ # 打开图片
+ with Image.open(image_path) as img:
+ # 转换为RGB模式(处理RGBA、P等模式)
+ if img.mode in ('RGBA', 'LA', 'P'):
+ # 创建白色背景
+ background = Image.new('RGB', img.size, (255, 255, 255))
+ if img.mode == 'P':
+ img = img.convert('RGBA')
+ background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
+ img = background
+ elif img.mode != 'RGB':
+ img = img.convert('RGB')
+
+ # 计算压缩质量
+ quality = 85
+ while quality >= 10:
+ # 创建内存缓冲区
+ buffer = io.BytesIO()
+ # 保存图片到缓冲区
+ img.save(buffer, format='JPEG', quality=quality, optimize=True)
+
+ # 检查压缩后大小
+ if buffer.tell() <= max_size_kb * 1024:
+ break
+ quality -= 10
+
+ # 如果quality降到10以下还是太大,强制使用quality=10
+ if quality < 10:
+ quality = 10
+ buffer = io.BytesIO()
+ img.save(buffer, format='JPEG', quality=quality, optimize=True)
+
+ # 创建临时文件路径
+ temp_path = image_path + '.compressed.jpg'
+ with open(temp_path, 'wb') as f:
+ f.write(buffer.getvalue())
+
+ compressed_size_kb = os.path.getsize(temp_path) / 1024
+ print(f"压缩完成!原大小: {file_size_kb:.1f}KB -> 压缩后: {compressed_size_kb:.1f}KB (质量: {quality})")
+
+ return temp_path, True
+
+
+def cleanup_temp_file(temp_path, original_path):
+ """
+ 清理临时文件(如果不是原文件的话)
+ """
+ if temp_path != original_path and os.path.exists(temp_path):
+ try:
+ os.remove(temp_path)
+ except OSError:
+ pass
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/product_search.py b/skills/1688-product-search/scripts/product_search.py
new file mode 100644
index 00000000..a32e77b4
--- /dev/null
+++ b/skills/1688-product-search/scripts/product_search.py
@@ -0,0 +1,662 @@
+#!/usr/bin/env python3
+"""
+1688 商品搜索脚本
+支持9个核心接口:类目查询、关键词搜索、图片搜索、商品详情等
+"""
+
+import os
+import sys
+import json
+import argparse
+import requests
+import base64
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from scripts.auth import get_access_token, sign_request_hmac_sha1
+from scripts.image_utils import compress_image_if_needed, cleanup_temp_file
+
+# API 基础URL
+BASE_URL = "https://gw.open.1688.com/openapi"
+
+def get_app_credentials():
+ """获取应用凭证"""
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ raise Exception("Missing ALI1688_APP_KEY or ALI1688_APP_SECRET")
+
+ return app_key, app_secret, refresh_token, access_token
+
+def category_query(cate_id=0, language='en'):
+ """类目查询"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ params = {
+ 'categoryId': str(cate_id),
+ 'language': language,
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/category.translation.getById/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def keyword_search(keyword, country='en', begin_page=1, page_size=20, filter_param=None, sort_param=None):
+ """多语言关键词搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'keyword': keyword,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ if filter_param:
+ search_params['filter'] = filter_param
+ if sort_param:
+ search_params['sort'] = json.loads(sort_param) if isinstance(sort_param, str) else sort_param
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def image_search(image_id=None, country='en', begin_page=1, page_size=20,
+ image_path=None, image_url=None):
+ """多语言图片搜索
+
+ 支持三种图片来源(优先级:本地文件 > imageId > 图片URL):
+ - image_path: 本地图片文件路径,自动压缩(>300KB)→ base64 → 上传获取 imageId → 图搜
+ - image_id: 已有的 imageId,直接图搜
+ - image_url: 图片 URL,通过 imageAddress 字段直接图搜
+ """
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ if image_path:
+ # 本地图片:直接 base64 编码 → 上传获取 imageId → 图搜,不走 imageAddress 降级
+ uploaded_id = upload_image_and_get_id(image_path, app_key, app_secret, token)
+ if not uploaded_id:
+ raise Exception("本地图片上传失败,无法获取有效 imageId,请检查图片格式或网络连接")
+ search_params['imageId'] = uploaded_id
+ elif image_id:
+ search_params['imageId'] = image_id
+ elif image_url:
+ if 'alicdn.com' in image_url:
+ # alicdn.com 域名:直接用 imageAddress 图搜,无需下载
+ search_params['imageAddress'] = image_url
+ else:
+ # 非 alicdn.com 域名:先下载到本地,再走本地文件图搜流程
+ print(f"非 alicdn.com 域名图片,先下载到本地再图搜: {image_url}")
+ local_path = _download_image_to_temp(image_url)
+ uploaded_id = upload_image_and_get_id(local_path, app_key, app_secret, token)
+ try:
+ os.remove(local_path)
+ except OSError:
+ pass
+ if uploaded_id:
+ search_params['imageId'] = uploaded_id
+ else:
+ print("图片上传失败,降级使用 imageAddress 方式图搜")
+ search_params['imageAddress'] = image_url
+ else:
+ raise Exception("必须提供 image_path、image_id 或 image_url 之一")
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+
+def _download_image_to_temp(image_url: str) -> str:
+ """将图片URL下载到本地临时文件,返回本地文件路径"""
+ import tempfile
+ from urllib.parse import urlparse
+
+ parsed = urlparse(image_url)
+ suffix = os.path.splitext(parsed.path)[-1] or '.jpg'
+ # 只保留常见图片后缀,其他一律用 .jpg
+ if suffix.lower() not in ('.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp'):
+ suffix = '.jpg'
+
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_file:
+ local_path = tmp_file.name
+
+ response = requests.get(image_url, timeout=30, stream=True)
+ response.raise_for_status()
+ with open(local_path, 'wb') as f:
+ for chunk in response.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ size_kb = os.path.getsize(local_path) / 1024
+ print(f"图片已下载到本地: {local_path}({size_kb:.1f}KB)")
+ return local_path
+
+
+def upload_image_and_get_id(image_path, app_key, app_secret, token):
+ """将本地图片压缩(>300KB)→ base64 → 上传,返回 imageId(失败返回 None)"""
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ with open(compressed_path, 'rb') as f:
+ image_base64 = base64.b64encode(f.read()).decode('utf-8')
+
+ upload_params_json = json.dumps({'imageBase64': image_base64}, separators=(',', ':'))
+ params = {
+ 'uploadImageParam': upload_params_json,
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+ response.raise_for_status()
+ result = response.json()
+
+ image_id = result.get('result', {}).get('result')
+ if image_id and str(image_id) != '0':
+ print(f"图片上传成功,imageId: {image_id}")
+ return str(image_id)
+
+ print(f"图片上传返回无效 imageId,响应: {result}")
+ return None
+
+ finally:
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+def product_detail(offer_id, country='en', out_member_id="1"):
+ """多语言商品详情"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ detail_params = {
+ 'offerId': int(offer_id),
+ 'country': country,
+ 'outMemberId': out_member_id
+ }
+
+ params = {
+ 'offerDetailParam': json.dumps(detail_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.queryProductDetail/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def shop_search(seller_open_id, country='en', begin_page=1, page_size=20):
+ """多语言商品店铺搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'sellerOpenId': seller_open_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'searchParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.shopSearch/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def seller_offer_list(seller_open_id, country='en', begin_page=1, page_size=20):
+ """查询同店商品列表(按卖家ID查询该商家的所有商品)"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'sellerOpenId': seller_open_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.querySellerOfferList/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def offer_recommend(keyword, country='en', begin_page=1, page_size=20):
+ """多语言商品推荐"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ recommend_params = {
+ 'keyword': keyword,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'recommendOfferParam': json.dumps(recommend_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.offerRecommend/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def pool_product_pull(offer_pool_id, task_id, page_no=1, page_size=10,
+ cate_id=None, language='en', sort_field=None, sort_type=None):
+ """品池商品拉取(pool.product.pull)
+
+ 从业务定制的品池中拉取商品列表,需要有品池访问权限。
+ 分页查询时需固定同一个 taskId,每次分页都传同一个 taskId。
+
+ 参数:
+ offer_pool_id: 品池ID(必填,业务定制且有权限控制,随便传会报错)
+ task_id: 查询任务ID(必填,分页查询时需固定同一个 taskId)
+ page_no: 页码(必填,默认1)
+ page_size: 每页数量(必填,默认10)
+ cate_id: 类目ID(选填)
+ language: 语言(选填,默认 en)
+ sort_field: 排序字段(选填,order1m=最近1个月销售额,buyer1m=最近1个月买家数)
+ sort_type: 排序规则(选填,ASC/DESC)
+ """
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ pull_params = {
+ 'offerPoolId': int(offer_pool_id),
+ 'taskId': str(task_id),
+ 'pageNo': page_no,
+ 'pageSize': page_size,
+ 'language': language
+ }
+
+ if cate_id is not None:
+ pull_params['cateId'] = int(cate_id)
+ if sort_field:
+ pull_params['sortField'] = sort_field
+ if sort_type:
+ pull_params['sortType'] = sort_type
+
+ params = {
+ 'offerPoolQueryParam': json.dumps(pull_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/pool.product.pull/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def related_recommend(offer_id, language='zh', page_no=1, page_size=10):
+ """相关性商品推荐"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ related_params = {
+ 'offerId': str(offer_id),
+ 'language': language,
+ 'pageNo': page_no,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'relatedQueryParams': json.dumps(related_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.related.recommend/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def upload_image(image_path):
+ """上传图片获取imageId(自动压缩大于300KB的图片)"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ # 检查并压缩图片(如果需要)
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ # 读取(可能已压缩的)图片并转换为base64
+ with open(compressed_path, 'rb') as f:
+ image_data = f.read()
+ base64_image = base64.b64encode(image_data).decode('utf-8')
+
+ upload_params = {
+ 'imageBase64': base64_image
+ }
+
+ params = {
+ 'uploadImageParam': json.dumps(upload_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+ response.raise_for_status()
+ return response.json()
+
+ finally:
+ # 如果图片被压缩了,清理临时文件
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+def _print_offer_list(result, command):
+ """格式化展示商品列表,展示所有可用字段"""
+ # related-recommend 和 offer-recommend 的 result.result 直接是列表
+ if command in ('related-recommend', 'offer-recommend'):
+ items = result.get('result', {}).get('result', [])
+ total = len(items)
+ current_page = 1
+ total_page = 1
+ page_size = len(items)
+ else:
+ data = result.get('result', {}).get('result', {})
+ items = data.get('data', [])
+ total = data.get('totalRecords', len(items))
+ current_page = data.get('currentPage', 1)
+ total_page = data.get('totalPage', 1)
+ page_size = data.get('pageSize', len(items))
+
+ print(f"共找到 {total} 件商品(第 {current_page}/{total_page} 页,每页 {page_size} 件),展示前 {len(items)} 件:\n")
+ for index, item in enumerate(items, 1):
+ offer_id = item.get('offerId', '')
+ subject = item.get('subject', '') or item.get('offerTitle', '')
+ subject_trans = item.get('subjectTrans', '') or item.get('offerTitleTrans', '')
+ image_url = item.get('imageUrl', '')
+ detail_url = f"https://detail.1688.com/offer/{offer_id}.html"
+ promotion_url = item.get('promotionURL', detail_url)
+
+ # 价格信息
+ price_info = item.get('priceInfo', {}) or {}
+ price = price_info.get('price', 'N/A')
+ promotion_price = price_info.get('promotionPrice', '')
+ consign_price = price_info.get('consignPrice', '')
+
+ # 销售数据
+ month_sold = item.get('monthSold', 0)
+ repurchase_rate = item.get('repurchaseRate', 'N/A')
+ trade_score = item.get('tradeScore', '')
+ min_order_quantity = item.get('minOrderQuantity', '')
+
+ # 商家信息
+ seller_data = item.get('sellerDataInfo', {}) or {}
+ trade_medal = seller_data.get('tradeMedalLevel', '')
+ composite_score = seller_data.get('compositeServiceScore', '')
+ logistics_score = seller_data.get('logisticsExperienceScore', '')
+ dispute_score = seller_data.get('disputeComplaintScore', '')
+ after_sales_score = seller_data.get('afterSalesExperienceScore', '')
+
+ # 物流信息
+ shipping_info = item.get('productSimpleShippingInfo', {}) or {}
+ shipping_guarantee = shipping_info.get('shippingTimeGuarantee', '')
+ shipping_label = {'shipIn24Hours': '24小时发货', 'shipIn48Hours': '48小时发货'}.get(shipping_guarantee, shipping_guarantee)
+
+ # 商品标签
+ offer_identities = item.get('offerIdentities', [])
+ seller_identities = item.get('sellerIdentities', [])
+ is_one_psale = item.get('isOnePsale', False)
+ is_select = item.get('isSelect', False)
+ is_jxhy = item.get('isJxhy', False)
+ is_patent = item.get('isPatentProduct', False)
+ create_date = item.get('createDate', '')
+ modify_date = item.get('modifyDate', '')
+
+ # 类目信息
+ top_cate = item.get('topCategoryId', '')
+ second_cate = item.get('secondCategoryId', '')
+ third_cate = item.get('thirdCategoryId', '')
+
+ # 价格拼接
+ price_str = f"¥{price}"
+ if promotion_price:
+ price_str += f"(促销¥{promotion_price})"
+ if consign_price and consign_price != price:
+ price_str += f"(代发¥{consign_price})"
+
+ # 特性标签拼接
+ flags = []
+ if shipping_label:
+ flags.append(shipping_label)
+ if is_one_psale:
+ flags.append("一件代发")
+ if is_select:
+ flags.append("1688严选")
+ if is_jxhy:
+ flags.append("精选货源")
+
+ # 优先使用推广链接,否则用普通详情链接
+ display_url = promotion_url if (promotion_url and promotion_url != detail_url) else detail_url
+
+ print(f"{index}. {subject}")
+ if subject_trans:
+ print(f" EN: {subject_trans}")
+ print(f" 图片: {image_url}")
+ print(f" ID: {offer_id} | 链接: {display_url}")
+ print(f" 价格: {price_str} | 起订: {min_order_quantity}件")
+ print(f" 月销: {month_sold}件 | 复购: {repurchase_rate} | 店铺: {trade_score}分/{trade_medal}星 | 综合服务: {composite_score}")
+ if flags:
+ print(f" 特性: {' | '.join(flags)}")
+ if create_date:
+ print(f" 上架: {create_date[:10]} | 更新: {modify_date[:10]}")
+ print()
+
+
+def main():
+ parser = argparse.ArgumentParser(description='1688 Product Search Skill')
+ subparsers = parser.add_subparsers(dest='command', required=True)
+
+ # 类目查询
+ category_parser = subparsers.add_parser('category', help='Category query')
+ category_parser.add_argument('cate_id', type=int, nargs='?', default=0)
+ category_parser.add_argument('--language', default='en')
+
+ # 关键词搜索
+ keyword_parser = subparsers.add_parser('keyword-search', help='Keyword search')
+ keyword_parser.add_argument('keyword', help='Search keyword')
+ keyword_parser.add_argument('--country', default='en')
+ keyword_parser.add_argument('--beginPage', type=int, default=1)
+ keyword_parser.add_argument('--pageSize', type=int, default=20)
+ keyword_parser.add_argument('--filter', help='Filter conditions (comma separated)')
+ keyword_parser.add_argument('--sort', help='Sort parameter (JSON string)')
+
+ # 图片搜索
+ image_parser = subparsers.add_parser('image-search', help='Image search')
+ image_parser.add_argument('image_id', nargs='?', default=None,
+ help='Image ID(可选,与 --image-path / --image-url 三选一)')
+ image_parser.add_argument('--image-path', dest='image_path',
+ help='本地图片文件路径(自动压缩>300KB → base64 → 上传获取imageId → 图搜)')
+ image_parser.add_argument('--image-url', dest='image_url',
+ help='图片URL(通过 imageAddress 字段直接图搜)')
+ image_parser.add_argument('--country', default='en')
+ image_parser.add_argument('--beginPage', type=int, default=1)
+ image_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 商品详情
+ detail_parser = subparsers.add_parser('product-detail', help='Product detail')
+ detail_parser.add_argument('offer_id', help='Offer ID')
+ detail_parser.add_argument('--country', default='en')
+ detail_parser.add_argument('--outMemberId', help='Out Member ID (optional)')
+
+ # 店铺搜索
+ shop_parser = subparsers.add_parser('shop-search', help='Shop search')
+ shop_parser.add_argument('seller_open_id', help='Seller Open ID')
+ shop_parser.add_argument('--country', default='en')
+ shop_parser.add_argument('--beginPage', type=int, default=1)
+ shop_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 同店商品查询
+ seller_offers_parser = subparsers.add_parser('seller-offers', help='Query seller offer list (same shop products)')
+ seller_offers_parser.add_argument('seller_open_id', help='Seller Open ID')
+ seller_offers_parser.add_argument('--country', default='en')
+ seller_offers_parser.add_argument('--beginPage', type=int, default=1)
+ seller_offers_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 商品推荐
+ recommend_parser = subparsers.add_parser('offer-recommend', help='Offer recommend')
+ recommend_parser.add_argument('keyword', help='Keyword for recommendation')
+ recommend_parser.add_argument('--country', default='en')
+ recommend_parser.add_argument('--beginPage', type=int, default=1)
+ recommend_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 品池商品拉取
+ pool_parser = subparsers.add_parser('pool-pull', help='Pull products from offer pool')
+ pool_parser.add_argument('--pool-id', required=True, type=int, dest='offer_pool_id',
+ help='品池ID(必填,业务定制且有权限控制)')
+ pool_parser.add_argument('--task-id', required=True, dest='task_id',
+ help='查询任务ID(必填,分页查询时需固定同一个 taskId)')
+ pool_parser.add_argument('--page-no', type=int, default=1, dest='page_no', help='页码(默认1)')
+ pool_parser.add_argument('--page-size', type=int, default=10, dest='page_size', help='每页数量(默认10)')
+ pool_parser.add_argument('--cate-id', type=int, default=None, dest='cate_id', help='类目ID(选填)')
+ pool_parser.add_argument('--language', default='en', help='语言(默认en)')
+ pool_parser.add_argument('--sort-field', default=None, dest='sort_field',
+ help='排序字段:order1m(最近1个月销售额)/ buyer1m(最近1个月买家数)')
+ pool_parser.add_argument('--sort-type', default=None, dest='sort_type', help='排序规则:ASC/DESC')
+
+ # 相关推荐
+ related_parser = subparsers.add_parser('related-recommend', help='Related recommend')
+ related_parser.add_argument('offer_id', help='Offer ID')
+ related_parser.add_argument('--language', default='zh')
+ related_parser.add_argument('--pageNo', type=int, default=1)
+ related_parser.add_argument('--pageSize', type=int, default=10)
+
+ # 图片上传
+ upload_parser = subparsers.add_parser('upload-image', help='Upload image to get imageId (auto-compress if >300KB)')
+ upload_parser.add_argument('image_path', help='Local image file path')
+
+ args = parser.parse_args()
+
+ try:
+ if args.command == 'category':
+ result = category_query(args.cate_id, args.language)
+ elif args.command == 'keyword-search':
+ result = keyword_search(
+ args.keyword, args.country, args.beginPage, args.pageSize,
+ getattr(args, 'filter', None), getattr(args, 'sort', None)
+ )
+ elif args.command == 'image-search':
+ result = image_search(
+ image_id=args.image_id,
+ country=args.country,
+ begin_page=args.beginPage,
+ page_size=args.pageSize,
+ image_path=getattr(args, 'image_path', None),
+ image_url=getattr(args, 'image_url', None)
+ )
+ elif args.command == 'product-detail':
+ result = product_detail(args.offer_id, args.country, getattr(args, 'outMemberId', None))
+ elif args.command == 'shop-search':
+ result = shop_search(args.seller_open_id, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'seller-offers':
+ result = seller_offer_list(args.seller_open_id, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'offer-recommend':
+ result = offer_recommend(args.keyword, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'pool-pull':
+ result = pool_product_pull(
+ args.offer_pool_id, args.task_id,
+ page_no=args.page_no, page_size=args.page_size,
+ cate_id=args.cate_id, language=args.language,
+ sort_field=args.sort_field, sort_type=args.sort_type
+ )
+ elif args.command == 'related-recommend':
+ result = related_recommend(args.offer_id, args.language, args.pageNo, args.pageSize)
+ elif args.command == 'upload-image':
+ result = upload_image(args.image_path)
+
+ # 对商品列表类接口,额外格式化展示商品ID和链接
+ if args.command in ('keyword-search', 'image-search', 'shop-search', 'offer-recommend', 'related-recommend', 'seller-offers'):
+ _print_offer_list(result, args.command)
+ else:
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+
+ except Exception as e:
+ print(json.dumps({'error': str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/product_search_fixed.py b/skills/1688-product-search/scripts/product_search_fixed.py
new file mode 100644
index 00000000..043f7ea6
--- /dev/null
+++ b/skills/1688-product-search/scripts/product_search_fixed.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""
+1688 商品搜索脚本 - 修复版本
+支持9个核心接口:类目查询、关键词搜索、图片搜索、商品详情等
+修复了图片上传接口的签名问题
+"""
+
+import os
+import sys
+import json
+import argparse
+import requests
+import base64
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from scripts.auth_fixed import get_access_token, sign_request_hmac_sha1, sign_image_upload_request
+from scripts.image_utils import compress_image_if_needed, cleanup_temp_file
+
+# API 基础URL
+BASE_URL = "https://gw.open.1688.com/openapi"
+
+def get_app_credentials():
+ """获取应用凭证"""
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ raise Exception("Missing ALI1688_APP_KEY or ALI1688_APP_SECRET")
+
+ return app_key, app_secret, refresh_token, access_token
+
+def upload_image(image_path):
+ """上传图片获取imageId(自动压缩大于300KB的图片)- 修复签名版本"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ # 检查并压缩图片(如果需要)
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ # 读取(可能已压缩的)图片并转换为base64
+ with open(compressed_path, 'rb') as f:
+ image_data = f.read()
+ base64_image = base64.b64encode(image_data).decode('utf-8')
+
+ upload_params = {
+ 'imageBase64': base64_image
+ }
+
+ # 构造 uploadImageParam JSON 字符串(不进行额外编码)
+ upload_image_param_json = json.dumps(upload_params, separators=(',', ':'))
+
+ # 使用专用签名函数
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = sign_image_upload_request(url_path, upload_image_param_json, token, app_secret)
+
+ # 构造请求参数
+ params = {
+ 'uploadImageParam': upload_image_param_json,
+ 'access_token': token,
+ '_aop_signature': sign
+ }
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+ response.raise_for_status()
+
+ result = response.json()
+
+ # 提取 imageId
+ if 'result' in result and isinstance(result['result'], str):
+ return result['result']
+ else:
+ raise Exception(f"Failed to get imageId from upload response: {result}")
+
+ finally:
+ # 如果图片被压缩了,清理临时文件
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+def main():
+ parser = argparse.ArgumentParser(description='1688 Product Search Skill - Fixed Version')
+ subparsers = parser.add_subparsers(dest='command', required=True)
+
+ # 图片上传(修复版本)
+ upload_parser = subparsers.add_parser('upload-image', help='Upload image to get imageId (fixed signature)')
+ upload_parser.add_argument('image_path', help='Local image file path')
+
+ args = parser.parse_args()
+
+ try:
+ if args.command == 'upload-image':
+ result = upload_image(args.image_path)
+ print(json.dumps({"imageId": result}, ensure_ascii=False, indent=2))
+
+ except Exception as e:
+ print(json.dumps({'error': str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/product_search_updated.py b/skills/1688-product-search/scripts/product_search_updated.py
new file mode 100644
index 00000000..dc87a9cc
--- /dev/null
+++ b/skills/1688-product-search/scripts/product_search_updated.py
@@ -0,0 +1,514 @@
+#!/usr/bin/env python3
+"""
+1688 商品搜索脚本(增强版)
+支持9个核心接口:类目查询、关键词搜索、图片搜索、商品详情等
+新增功能:当检测到"图搜同款"、"找同款"等命令时,自动调用图片搜索接口
+"""
+
+import os
+import sys
+import json
+import argparse
+import requests
+import base64
+import re
+from urllib.parse import urlparse
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from scripts.auth import get_access_token, sign_request_hmac_sha1
+from scripts.image_utils import compress_image_if_needed
+
+# API 基础URL
+BASE_URL = "https://gw.open.1688.com/openapi"
+
+def get_app_credentials():
+ """获取应用凭证"""
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ raise Exception("Missing ALI1688_APP_KEY or ALI1688_APP_SECRET")
+
+ return app_key, app_secret, refresh_token, access_token
+
+def is_image_url(url):
+ """检查是否为有效的图片URL"""
+ try:
+ parsed = urlparse(url)
+ if not parsed.scheme or not parsed.netloc:
+ return False
+
+ # 检查常见的图片扩展名
+ image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']
+ path_lower = parsed.path.lower()
+ return any(path_lower.endswith(ext) for ext in image_extensions)
+ except:
+ return False
+
+def download_image_from_url(image_url, local_path):
+ """从URL下载图片到本地"""
+ try:
+ response = requests.get(image_url, timeout=30)
+ response.raise_for_status()
+
+ # 检查响应内容类型
+ content_type = response.headers.get('content-type', '').lower()
+ if 'image' not in content_type:
+ # 如果没有content-type,尝试通过文件扩展名判断
+ if not is_image_url(image_url):
+ raise Exception("URL does not appear to be an image")
+
+ with open(local_path, 'wb') as f:
+ f.write(response.content)
+ return True
+ except Exception as e:
+ raise Exception(f"Failed to download image from URL: {str(e)}")
+
+def upload_image(image_path):
+ """上传图片获取imageId(自动压缩大于300KB的图片)"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ # 检查并压缩图片(如果需要)
+ compressed_path, was_compressed = compress_image_if_needed(image_path, max_size_kb=300)
+
+ try:
+ # 读取(可能已压缩的)图片并转换为base64
+ with open(compressed_path, 'rb') as f:
+ image_data = f.read()
+ base64_image = base64.b64encode(image_data).decode('utf-8')
+
+ upload_params = {
+ 'imageBase64': base64_image
+ }
+
+ params = {
+ 'uploadImageParam': json.dumps(upload_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.image.upload/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=30)
+ response.raise_for_status()
+ result = response.json()
+
+ # 提取imageId
+ if 'result' in result and isinstance(result['result'], str):
+ return result['result']
+ else:
+ raise Exception("Failed to get imageId from upload response")
+
+ finally:
+ # 如果图片被压缩了,清理临时文件
+ if was_compressed and os.path.exists(compressed_path):
+ try:
+ os.remove(compressed_path)
+ except OSError:
+ pass
+
+def image_search_by_url(image_url, country='en', begin_page=1, page_size=20):
+ """通过图片URL进行图片搜索"""
+ # 创建临时文件路径
+ temp_image_path = "/tmp/1688_search_temp_image.jpg"
+
+ try:
+ # 下载图片
+ download_image_from_url(image_url, temp_image_path)
+
+ # 上传图片获取imageId
+ image_id = upload_image(temp_image_path)
+
+ # 使用imageId进行图片搜索
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'imageId': image_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+ finally:
+ # 清理临时文件
+ if os.path.exists(temp_image_path):
+ try:
+ os.remove(temp_image_path)
+ except OSError:
+ pass
+
+def smart_search(query, country='en', begin_page=1, page_size=20):
+ """
+ 智能搜索函数
+ - 如果query包含图片URL且包含"图搜同款"、"找同款"等关键词,执行图片搜索
+ - 否则执行关键词搜索
+ """
+ # 定义触发图片搜索的关键词
+ image_search_keywords = [
+ '图搜同款', '找同款', '图片搜', '以图搜', '图片搜索',
+ '同款', '相似款', '类似款', '找相似', '找类似'
+ ]
+
+ # 检查是否包含图片搜索关键词
+ should_use_image_search = any(keyword in query for keyword in image_search_keywords)
+
+ # 检查是否包含图片URL
+ url_pattern = r'https?://[^\s]+'
+ urls = re.findall(url_pattern, query)
+ image_urls = [url for url in urls if is_image_url(url)]
+
+ if should_use_image_search and image_urls:
+ # 使用第一个有效的图片URL进行图片搜索
+ return image_search_by_url(image_urls[0], country, begin_page, page_size)
+ else:
+ # 执行关键词搜索(移除URL和图片搜索关键词)
+ clean_query = query
+ for url in urls:
+ clean_query = clean_query.replace(url, '')
+ for keyword in image_search_keywords:
+ clean_query = clean_query.replace(keyword, '')
+ clean_query = clean_query.strip()
+
+ if not clean_query:
+ clean_query = "牛仔夹克" # 默认关键词
+
+ return keyword_search(clean_query, country, begin_page, page_size)
+
+def category_query(cate_id=0, language='en'):
+ """类目查询"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ params = {
+ 'categoryId': str(cate_id),
+ 'language': language,
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/category.translation.getById/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def keyword_search(keyword, country='en', begin_page=1, page_size=20, filter_param=None, sort_param=None):
+ """多语言关键词搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'keyword': keyword,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ if filter_param:
+ search_params['filter'] = filter_param
+ if sort_param:
+ search_params['sort'] = json.loads(sort_param) if isinstance(sort_param, str) else sort_param
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def image_search(image_id, country='en', begin_page=1, page_size=20):
+ """多语言图片搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'imageId': image_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'offerQueryParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.imageQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def product_detail(offer_id, country='en', out_member_id="1"):
+ """多语言商品详情"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ detail_params = {
+ 'offerId': int(offer_id),
+ 'country': country,
+ 'outMemberId': out_member_id
+ }
+
+ params = {
+ 'offerDetailParam': json.dumps(detail_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.queryProductDetail/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def shop_search(seller_open_id, country='en', begin_page=1, page_size=20):
+ """多语言商品店铺搜索"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ search_params = {
+ 'sellerOpenId': seller_open_id,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'searchParam': json.dumps(search_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.shopSearch/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def offer_recommend(keyword, country='en', begin_page=1, page_size=20):
+ """多语言商品推荐"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ recommend_params = {
+ 'keyword': keyword,
+ 'country': country,
+ 'beginPage': begin_page,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'recommendOfferParam': json.dumps(recommend_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.offerRecommend/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def search_navigation(keyword, country='en'):
+ """多语言搜索导航"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ nav_params = {
+ 'keyword': keyword,
+ 'country': country
+ }
+
+ params = {
+ 'navParam': json.dumps(nav_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.search.keywordSNQuery/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def related_recommend(offer_id, language='zh', page_no=1, page_size=10):
+ """相关性商品推荐"""
+ app_key, app_secret, refresh_token, access_token = get_app_credentials()
+ token = get_access_token(app_key, app_secret, refresh_token, access_token)
+
+ related_params = {
+ 'offerId': str(offer_id),
+ 'language': language,
+ 'pageNo': page_no,
+ 'pageSize': page_size
+ }
+
+ params = {
+ 'relatedQueryParams': json.dumps(related_params, separators=(',', ':')),
+ 'access_token': token
+ }
+
+ url_path = f"param2/1/com.alibaba.fenxiao.crossborder/product.related.recommend/{app_key}"
+ sign = sign_request_hmac_sha1(url_path, params, app_secret)
+ params['_aop_signature'] = sign
+
+ url = f"{BASE_URL}/{url_path}"
+ response = requests.post(url, data=params, timeout=15)
+ response.raise_for_status()
+ return response.json()
+
+def main():
+ parser = argparse.ArgumentParser(description='1688 Product Search Skill (Enhanced)')
+ subparsers = parser.add_subparsers(dest='command', required=True)
+
+ # 智能搜索(新增)
+ smart_parser = subparsers.add_parser('smart-search', help='Smart search (auto-detect image search vs keyword search)')
+ smart_parser.add_argument('query', help='Search query (can include image URL and keywords like "图搜同款")')
+ smart_parser.add_argument('--country', default='en')
+ smart_parser.add_argument('--beginPage', type=int, default=1)
+ smart_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 类目查询
+ category_parser = subparsers.add_parser('category', help='Category query')
+ category_parser.add_argument('cate_id', type=int, nargs='?', default=0)
+ category_parser.add_argument('--language', default='en')
+
+ # 关键词搜索
+ keyword_parser = subparsers.add_parser('keyword-search', help='Keyword search')
+ keyword_parser.add_argument('keyword', help='Search keyword')
+ keyword_parser.add_argument('--country', default='en')
+ keyword_parser.add_argument('--beginPage', type=int, default=1)
+ keyword_parser.add_argument('--pageSize', type=int, default=20)
+ keyword_parser.add_argument('--filter', help='Filter conditions (comma separated)')
+ keyword_parser.add_argument('--sort', help='Sort parameter (JSON string)')
+
+ # 图片搜索
+ image_parser = subparsers.add_parser('image-search', help='Image search')
+ image_parser.add_argument('image_id', help='Image ID')
+ image_parser.add_argument('--country', default='en')
+ image_parser.add_argument('--beginPage', type=int, default=1)
+ image_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 图片搜索(通过URL)
+ image_url_parser = subparsers.add_parser('image-search-url', help='Image search by URL')
+ image_url_parser.add_argument('image_url', help='Image URL')
+ image_url_parser.add_argument('--country', default='en')
+ image_url_parser.add_argument('--beginPage', type=int, default=1)
+ image_url_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 商品详情
+ detail_parser = subparsers.add_parser('product-detail', help='Product detail')
+ detail_parser.add_argument('offer_id', help='Offer ID')
+ detail_parser.add_argument('--country', default='en')
+ detail_parser.add_argument('--outMemberId', help='Out Member ID (optional)')
+
+ # 店铺搜索
+ shop_parser = subparsers.add_parser('shop-search', help='Shop search')
+ shop_parser.add_argument('seller_open_id', help='Seller Open ID')
+ shop_parser.add_argument('--country', default='en')
+ shop_parser.add_argument('--beginPage', type=int, default=1)
+ shop_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 商品推荐
+ recommend_parser = subparsers.add_parser('offer-recommend', help='Offer recommend')
+ recommend_parser.add_argument('keyword', help='Keyword for recommendation')
+ recommend_parser.add_argument('--country', default='en')
+ recommend_parser.add_argument('--beginPage', type=int, default=1)
+ recommend_parser.add_argument('--pageSize', type=int, default=20)
+
+ # 搜索导航
+ nav_parser = subparsers.add_parser('search-navigation', help='Search navigation')
+ nav_parser.add_argument('keyword', help='Keyword for navigation')
+ nav_parser.add_argument('--country', default='en')
+
+ # 相关推荐
+ related_parser = subparsers.add_parser('related-recommend', help='Related recommend')
+ related_parser.add_argument('offer_id', help='Offer ID')
+ related_parser.add_argument('--language', default='zh')
+ related_parser.add_argument('--pageNo', type=int, default=1)
+ related_parser.add_argument('--pageSize', type=int, default=10)
+
+ # 图片上传
+ upload_parser = subparsers.add_parser('upload-image', help='Upload image to get imageId (auto-compress if >300KB)')
+ upload_parser.add_argument('image_path', help='Local image file path')
+
+ args = parser.parse_args()
+
+ try:
+ if args.command == 'smart-search':
+ result = smart_search(args.query, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'category':
+ result = category_query(args.cate_id, args.language)
+ elif args.command == 'keyword-search':
+ result = keyword_search(
+ args.keyword, args.country, args.beginPage, args.pageSize,
+ getattr(args, 'filter', None), getattr(args, 'sort', None)
+ )
+ elif args.command == 'image-search':
+ result = image_search(args.image_id, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'image-search-url':
+ result = image_search_by_url(args.image_url, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'product-detail':
+ result = product_detail(args.offer_id, args.country, getattr(args, 'outMemberId', None))
+ elif args.command == 'shop-search':
+ result = shop_search(args.seller_open_id, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'offer-recommend':
+ result = offer_recommend(args.keyword, args.country, args.beginPage, args.pageSize)
+ elif args.command == 'search-navigation':
+ result = search_navigation(args.keyword, args.country)
+ elif args.command == 'related-recommend':
+ result = related_recommend(args.offer_id, args.language, args.pageNo, args.pageSize)
+ elif args.command == 'upload-image':
+ result = upload_image(args.image_path)
+
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+
+ except Exception as e:
+ print(json.dumps({'error': str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/skills/1688-product-search/scripts/smart_recommend.py b/skills/1688-product-search/scripts/smart_recommend.py
new file mode 100644
index 00000000..a70eff70
--- /dev/null
+++ b/skills/1688-product-search/scripts/smart_recommend.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""
+1688 智能推荐处理器
+自动处理用户关于推荐商品的请求
+"""
+
+import os
+import sys
+import json
+import argparse
+import subprocess
+
+# 添加 scripts 目录到 Python 路径
+sys.path.insert(0, os.path.dirname(__file__))
+
+def get_app_credentials():
+ """获取应用凭证"""
+ app_key = os.getenv('ALI1688_APP_KEY')
+ app_secret = os.getenv('ALI1688_APP_SECRET')
+ refresh_token = os.getenv('ALI1688_REFRESH_TOKEN')
+ access_token = os.getenv('ALI1688_ACCESS_TOKEN')
+
+ if not app_key or not app_secret:
+ raise Exception("Missing ALI1688_APP_KEY or ALI1688_APP_SECRET")
+
+ return app_key, app_secret, refresh_token, access_token
+
+def smart_recommend(query=None, category_id=None, country='en', page_size=20):
+ """
+ 智能推荐商品
+ - 如果有具体查询词,使用关键词推荐
+ - 如果有类目ID,使用类目推荐
+ - 如果都没有,使用通用推荐
+ """
+ try:
+ # 获取凭证验证
+ get_app_credentials()
+
+ if category_id:
+ # 如果有类目ID,先获取类目名称,然后用类目名称做推荐
+ result = subprocess.run([
+ sys.executable, 'product_search.py', 'category', str(category_id), '--language', 'zh'
+ ], capture_output=True, text=True, cwd=os.path.dirname(__file__))
+
+ if result.returncode == 0:
+ category_data = json.loads(result.stdout)
+ if category_data.get('result', {}).get('chineseName'):
+ category_name = category_data['result']['chineseName']
+ query = category_name
+
+ # 如果没有查询词,使用通用关键词
+ if not query:
+ query = "热门商品"
+
+ # 调用 offer-recommend 接口
+ result = subprocess.run([
+ sys.executable, 'product_search.py', 'offer-recommend', query,
+ '--country', country, '--pageSize', str(page_size)
+ ], capture_output=True, text=True, cwd=os.path.dirname(__file__))
+
+ if result.returncode == 0:
+ return json.loads(result.stdout)
+ else:
+ error_msg = result.stderr if result.stderr else "Unknown error"
+ raise Exception(f"Recommendation failed: {error_msg}")
+
+ except Exception as e:
+ return {'error': str(e)}
+
+def main():
+ parser = argparse.ArgumentParser(description='1688 Smart Recommendation')
+ parser.add_argument('--query', help='Search query or category name')
+ parser.add_argument('--category-id', type=int, help='Category ID for recommendation')
+ parser.add_argument('--country', default='en', help='Country/language code')
+ parser.add_argument('--page-size', type=int, default=20, help='Number of results')
+
+ args = parser.parse_args()
+
+ try:
+ result = smart_recommend(
+ query=args.query,
+ category_id=args.category_id,
+ country=args.country,
+ page_size=args.page_size
+ )
+ print(json.dumps(result, ensure_ascii=False, indent=2))
+ except Exception as e:
+ print(json.dumps({'error': str(e)}, ensure_ascii=False, indent=2))
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/skills/abm-churn-prevention/SKILL.md b/skills/abm-churn-prevention/SKILL.md
new file mode 100644
index 00000000..1056efd0
--- /dev/null
+++ b/skills/abm-churn-prevention/SKILL.md
@@ -0,0 +1,424 @@
+---
+name: churn-prevention
+description: "When the user wants to reduce churn, build cancellation flows, set up save offers, recover failed payments, or implement retention strategies. Also use when the user mentions 'churn,' 'cancel flow,' 'offboarding,' 'save offer,' 'dunning,' 'failed payment recovery,' 'win-back,' 'retention,' 'exit survey,' 'pause subscription,' 'involuntary churn,' 'people keep canceling,' 'churn rate is too high,' 'how do I keep users,' or 'customers are leaving.' Use this whenever someone is losing subscribers or wants to build systems to prevent it. For post-cancel win-back email sequences, see email-sequence. For in-app upgrade paywalls, see paywall-upgrade-cro."
+metadata:
+ version: 1.1.0
+---
+
+# Churn Prevention
+
+You are an expert in SaaS retention and churn prevention. Your goal is to help reduce both voluntary churn (customers choosing to cancel) and involuntary churn (failed payments) through well-designed cancel flows, dynamic save offers, proactive retention, and dunning strategies.
+
+## Before Starting
+
+**Check for product marketing context first:**
+If `.agents/product-marketing-context.md` exists (or `.claude/product-marketing-context.md` in older setups), read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
+
+Gather this context (ask if not provided):
+
+### 1. Current Churn Situation
+- What's your monthly churn rate? (Voluntary vs. involuntary if known)
+- How many active subscribers?
+- What's the average MRR per customer?
+- Do you have a cancel flow today, or does cancel happen instantly?
+
+### 2. Billing & Platform
+- What billing provider? (Stripe, Chargebee, Paddle, Recurly, Braintree)
+- Monthly, annual, or both billing intervals?
+- Do you support plan pausing or downgrades?
+- Any existing retention tooling? (Churnkey, ProsperStack, Raaft)
+
+### 3. Product & Usage Data
+- Do you track feature usage per user?
+- Can you identify engagement drop-offs?
+- Do you have cancellation reason data from past churns?
+- What's your activation metric? (What do retained users do that churned users don't?)
+
+### 4. Constraints
+- B2B or B2C? (Affects flow design)
+- Self-serve cancellation required? (Some regulations mandate easy cancel)
+- Brand tone for offboarding? (Empathetic, direct, playful)
+
+---
+
+## How This Skill Works
+
+Churn has two types requiring different strategies:
+
+| Type | Cause | Solution |
+|------|-------|----------|
+| **Voluntary** | Customer chooses to cancel | Cancel flows, save offers, exit surveys |
+| **Involuntary** | Payment fails | Dunning emails, smart retries, card updaters |
+
+Voluntary churn is typically 50-70% of total churn. Involuntary churn is 30-50% but is often easier to fix.
+
+This skill supports three modes:
+
+1. **Build a cancel flow** — Design from scratch with survey, save offers, and confirmation
+2. **Optimize an existing flow** — Analyze cancel data and improve save rates
+3. **Set up dunning** — Failed payment recovery with retries and email sequences
+
+---
+
+## Cancel Flow Design
+
+### The Cancel Flow Structure
+
+Every cancel flow follows this sequence:
+
+```
+Trigger → Survey → Dynamic Offer → Confirmation → Post-Cancel
+```
+
+**Step 1: Trigger**
+Customer clicks "Cancel subscription" in account settings.
+
+**Step 2: Exit Survey**
+Ask why they're cancelling. This determines which save offer to show.
+
+**Step 3: Dynamic Save Offer**
+Present a targeted offer based on their reason (discount, pause, downgrade, etc.)
+
+**Step 4: Confirmation**
+If they still want to cancel, confirm clearly with end-of-billing-period messaging.
+
+**Step 5: Post-Cancel**
+Set expectations, offer easy reactivation path, trigger win-back sequence.
+
+### Exit Survey Design
+
+The exit survey is the foundation. Good reason categories:
+
+| Reason | What It Tells You |
+|--------|-------------------|
+| Too expensive | Price sensitivity, may respond to discount or downgrade |
+| Not using it enough | Low engagement, may respond to pause or onboarding help |
+| Missing a feature | Product gap, show roadmap or workaround |
+| Switching to competitor | Competitive pressure, understand what they offer |
+| Technical issues / bugs | Product quality, escalate to support |
+| Temporary / seasonal need | Usage pattern, offer pause |
+| Business closed / changed | Unavoidable, learn and let go gracefully |
+| Other | Catch-all, include free text field |
+
+**Survey best practices:**
+- 1 question, single-select with optional free text
+- 5-8 reason options max (avoid decision fatigue)
+- Put most common reasons first (review data quarterly)
+- Don't make it feel like a guilt trip
+- "Help us improve" framing works better than "Why are you leaving?"
+
+### Dynamic Save Offers
+
+The key insight: **match the offer to the reason.** A discount won't save someone who isn't using the product. A feature roadmap won't save someone who can't afford it.
+
+**Offer-to-reason mapping:**
+
+| Cancel Reason | Primary Offer | Fallback Offer |
+|---------------|---------------|----------------|
+| Too expensive | Discount (20-30% for 2-3 months) | Downgrade to lower plan |
+| Not using it enough | Pause (1-3 months) | Free onboarding session |
+| Missing feature | Roadmap preview + timeline | Workaround guide |
+| Switching to competitor | Competitive comparison + discount | Feedback session |
+| Technical issues | Escalate to support immediately | Credit + priority fix |
+| Temporary / seasonal | Pause subscription | Downgrade temporarily |
+| Business closed | Skip offer (respect the situation) | — |
+
+### Save Offer Types
+
+**Discount**
+- 20-30% off for 2-3 months is the sweet spot
+- Avoid 50%+ discounts (trains customers to cancel for deals)
+- Time-limit the offer ("This offer expires when you leave this page")
+- Show the dollar amount saved, not just the percentage
+
+**Pause subscription**
+- 1-3 month pause maximum (longer pauses rarely reactivate)
+- 60-80% of pausers eventually return to active
+- Auto-reactivation with advance notice email
+- Keep their data and settings intact
+
+**Plan downgrade**
+- Offer a lower tier instead of full cancellation
+- Show what they keep vs. what they lose
+- Position as "right-size your plan" not "downgrade"
+- Easy path back up when ready
+
+**Feature unlock / extension**
+- Unlock a premium feature they haven't tried
+- Extend trial of a higher tier
+- Works best for "not getting enough value" reasons
+
+**Personal outreach**
+- For high-value accounts (top 10-20% by MRR)
+- Route to customer success for a call
+- Personal email from founder for smaller companies
+
+### Cancel Flow UI Patterns
+
+```
+┌─────────────────────────────────────┐
+│ We're sorry to see you go │
+│ │
+│ What's the main reason you're │
+│ cancelling? │
+│ │
+│ ○ Too expensive │
+│ ○ Not using it enough │
+│ ○ Missing a feature I need │
+│ ○ Switching to another tool │
+│ ○ Technical issues │
+│ ○ Temporary / don't need right now │
+│ ○ Other: [____________] │
+│ │
+│ [Continue] │
+│ [Never mind, keep my subscription] │
+└─────────────────────────────────────┘
+ ↓ (selects "Too expensive")
+┌─────────────────────────────────────┐
+│ What if we could help? │
+│ │
+│ We'd love to keep you. Here's a │
+│ special offer: │
+│ │
+│ ┌───────────────────────────────┐ │
+│ │ 25% off for the next 3 months│ │
+│ │ Save $XX/month │ │
+│ │ │ │
+│ │ [Accept Offer] │ │
+│ └───────────────────────────────┘ │
+│ │
+│ Or switch to [Basic Plan] at │
+│ $X/month → │
+│ │
+│ [No thanks, continue cancelling] │
+└─────────────────────────────────────┘
+```
+
+**UI principles:**
+- Keep the "continue cancelling" option visible (no dark patterns)
+- One primary offer + one fallback, not a wall of options
+- Show specific dollar savings, not abstract percentages
+- Use the customer's name and account data when possible
+- Mobile-friendly (many cancellations happen on mobile)
+
+For detailed cancel flow patterns by industry and billing provider, see [references/cancel-flow-patterns.md](references/cancel-flow-patterns.md).
+
+---
+
+## Churn Prediction & Proactive Retention
+
+The best save happens before the customer ever clicks "Cancel."
+
+### Risk Signals
+
+Track these leading indicators of churn:
+
+| Signal | Risk Level | Timeframe |
+|--------|-----------|-----------|
+| Login frequency drops 50%+ | High | 2-4 weeks before cancel |
+| Key feature usage stops | High | 1-3 weeks before cancel |
+| Support tickets spike then stop | High | 1-2 weeks before cancel |
+| Email open rates decline | Medium | 2-6 weeks before cancel |
+| Billing page visits increase | High | Days before cancel |
+| Team seats removed | High | 1-2 weeks before cancel |
+| Data export initiated | Critical | Days before cancel |
+| NPS score drops below 6 | Medium | 1-3 months before cancel |
+
+### Health Score Model
+
+Build a simple health score (0-100) from weighted signals:
+
+```
+Health Score = (
+ Login frequency score × 0.30 +
+ Feature usage score × 0.25 +
+ Support sentiment × 0.15 +
+ Billing health × 0.15 +
+ Engagement score × 0.15
+)
+```
+
+| Score | Status | Action |
+|-------|--------|--------|
+| 80-100 | Healthy | Upsell opportunities |
+| 60-79 | Needs attention | Proactive check-in |
+| 40-59 | At risk | Intervention campaign |
+| 0-39 | Critical | Personal outreach |
+
+### Proactive Interventions
+
+**Before they think about cancelling:**
+
+| Trigger | Intervention |
+|---------|-------------|
+| Usage drop >50% for 2 weeks | "We noticed you haven't used [feature]. Need help?" email |
+| Approaching plan limit | Upgrade nudge (not a wall — paywall-upgrade-cro handles this) |
+| No login for 14 days | Re-engagement email with recent product updates |
+| NPS detractor (0-6) | Personal follow-up within 24 hours |
+| Support ticket unresolved >48h | Escalation + proactive status update |
+| Annual renewal in 30 days | Value recap email + renewal confirmation |
+
+---
+
+## Involuntary Churn: Payment Recovery
+
+Failed payments cause 30-50% of all churn but are the most recoverable.
+
+### The Dunning Stack
+
+```
+Pre-dunning → Smart retry → Dunning emails → Grace period → Hard cancel
+```
+
+### Pre-Dunning (Prevent Failures)
+
+- **Card expiry alerts**: Email 30, 15, and 7 days before card expires
+- **Backup payment method**: Prompt for a second payment method at signup
+- **Card updater services**: Visa/Mastercard auto-update programs (reduces hard declines 30-50%)
+- **Pre-billing notification**: Email 3-5 days before charge for annual plans
+
+### Smart Retry Logic
+
+Not all failures are the same. Retry strategy by decline type:
+
+| Decline Type | Examples | Retry Strategy |
+|-------------|----------|----------------|
+| Soft decline (temporary) | Insufficient funds, processor timeout | Retry 3-5 times over 7-10 days |
+| Hard decline (permanent) | Card stolen, account closed | Don't retry — ask for new card |
+| Authentication required | 3D Secure, SCA | Send customer to update payment |
+
+**Retry timing best practices:**
+- Retry 1: 24 hours after failure
+- Retry 2: 3 days after failure
+- Retry 3: 5 days after failure
+- Retry 4: 7 days after failure (with dunning email escalation)
+- After 4 retries: Hard cancel with reactivation path
+
+**Smart retry tip:** Retry on the day of the month the payment originally succeeded (if Day 1 worked before, retry on Day 1). Stripe Smart Retries handles this automatically.
+
+### Dunning Email Sequence
+
+| Email | Timing | Tone | Content |
+|-------|--------|------|---------|
+| 1 | Day 0 (failure) | Friendly alert | "Your payment didn't go through. Update your card." |
+| 2 | Day 3 | Helpful reminder | "Quick reminder — update your payment to keep access." |
+| 3 | Day 7 | Urgency | "Your account will be paused in 3 days. Update now." |
+| 4 | Day 10 | Final warning | "Last chance to keep your account active." |
+
+**Dunning email best practices:**
+- Direct link to payment update page (no login required if possible)
+- Show what they'll lose (their data, their team's access)
+- Don't blame ("your payment failed" not "you failed to pay")
+- Include support contact for help
+- Plain text performs better than designed emails for dunning
+
+### Recovery Benchmarks
+
+| Metric | Poor | Average | Good |
+|--------|------|---------|------|
+| Soft decline recovery | <40% | 50-60% | 70%+ |
+| Hard decline recovery | <10% | 20-30% | 40%+ |
+| Overall payment recovery | <30% | 40-50% | 60%+ |
+| Pre-dunning prevention | None | 10-15% | 20-30% |
+
+For the complete dunning playbook with provider-specific setup, see [references/dunning-playbook.md](references/dunning-playbook.md).
+
+---
+
+## Metrics & Measurement
+
+### Key Churn Metrics
+
+| Metric | Formula | Target |
+|--------|---------|--------|
+| Monthly churn rate | Churned customers / Start-of-month customers | <5% B2C, <2% B2B |
+| Revenue churn (net) | (Lost MRR - Expansion MRR) / Start MRR | Negative (net expansion) |
+| Cancel flow save rate | Saved / Total cancel sessions | 25-35% |
+| Offer acceptance rate | Accepted offers / Shown offers | 15-25% |
+| Pause reactivation rate | Reactivated / Total paused | 60-80% |
+| Dunning recovery rate | Recovered / Total failed payments | 50-60% |
+| Time to cancel | Days from first churn signal to cancel | Track trend |
+
+### Cohort Analysis
+
+Segment churn by:
+- **Acquisition channel** — Which channels bring stickier customers?
+- **Plan type** — Which plans churn most?
+- **Tenure** — When do most cancellations happen? (30, 60, 90 days?)
+- **Cancel reason** — Which reasons are growing?
+- **Save offer type** — Which offers work best for which segments?
+
+### Cancel Flow A/B Tests
+
+Test one variable at a time:
+
+| Test | Hypothesis | Metric |
+|------|-----------|--------|
+| Discount % (20% vs 30%) | Higher discount saves more | Save rate, LTV impact |
+| Pause duration (1 vs 3 months) | Longer pause increases return rate | Reactivation rate |
+| Survey placement (before vs after offer) | Survey-first personalizes offers | Save rate |
+| Offer presentation (modal vs full page) | Full page gets more attention | Save rate |
+| Copy tone (empathetic vs direct) | Empathetic reduces friction | Save rate |
+
+**How to run cancel flow experiments:** Use the **ab-test-setup** skill to design statistically rigorous tests. PostHog is a good fit for cancel flow experiments — its feature flags can split users into different flows server-side, and its funnel analytics track each step of the cancel flow (survey → offer → accept/decline → confirm). See the [PostHog integration guide](../../tools/integrations/posthog.md) for setup.
+
+---
+
+## Common Mistakes
+
+- **No cancel flow at all** — Instant cancel leaves money on the table. Even a simple survey + one offer saves 10-15%
+- **Making cancellation hard to find** — Hidden cancel buttons breed resentment and bad reviews. Many jurisdictions require easy cancellation (FTC Click-to-Cancel rule)
+- **Same offer for every reason** — A blanket discount doesn't address "missing feature" or "not using it"
+- **Discounts too deep** — 50%+ discounts train customers to cancel-and-return for deals
+- **Ignoring involuntary churn** — Often 30-50% of total churn and the easiest to fix
+- **No dunning emails** — Letting payment failures silently cancel accounts
+- **Guilt-trip copy** — "Are you sure you want to abandon us?" damages brand trust
+- **Not tracking save offer LTV** — A "saved" customer who churns 30 days later wasn't really saved
+- **Pausing too long** — Pauses beyond 3 months rarely reactivate. Set limits.
+- **No post-cancel path** — Make reactivation easy and trigger win-back emails, because some churned users will want to come back
+
+---
+
+## Tool Integrations
+
+For implementation, see the [tools registry](../../tools/REGISTRY.md).
+
+### Retention Platforms
+
+| Tool | Best For | Key Feature |
+|------|----------|-------------|
+| **Churnkey** | Full cancel flow + dunning | AI-powered adaptive offers, 34% avg save rate |
+| **ProsperStack** | Cancel flows with analytics | Advanced rules engine, Stripe/Chargebee integration |
+| **Raaft** | Simple cancel flow builder | Easy setup, good for early-stage |
+| **Chargebee Retention** | Chargebee customers | Native integration, was Brightback |
+
+### Billing Providers (Dunning)
+
+| Provider | Smart Retries | Dunning Emails | Card Updater |
+|----------|:------------:|:--------------:|:------------:|
+| **Stripe** | Built-in (Smart Retries) | Built-in | Automatic |
+| **Chargebee** | Built-in | Built-in | Via gateway |
+| **Paddle** | Built-in | Built-in | Managed |
+| **Recurly** | Built-in | Built-in | Built-in |
+| **Braintree** | Manual config | Manual | Via gateway |
+
+### Related CLI Tools
+
+| Tool | Use For |
+|------|---------|
+| `stripe` | Subscription management, dunning config, payment retries |
+| `customer-io` | Dunning email sequences, retention campaigns |
+| `posthog` | Cancel flow A/B tests via feature flags, funnel analytics |
+| `mixpanel` / `ga4` | Usage tracking, churn signal analysis |
+| `segment` | Event routing for health scoring |
+
+---
+
+## Related Skills
+
+- **email-sequence**: For win-back email sequences after cancellation
+- **paywall-upgrade-cro**: For in-app upgrade moments and trial expiration
+- **pricing-strategy**: For plan structure and annual discount strategy
+- **onboarding-cro**: For activation to prevent early churn
+- **analytics-tracking**: For setting up churn signal events
+- **ab-test-setup**: For testing cancel flow variations with statistical rigor
diff --git a/skills/abm-churn-prevention/_meta.json b/skills/abm-churn-prevention/_meta.json
new file mode 100644
index 00000000..c640d843
--- /dev/null
+++ b/skills/abm-churn-prevention/_meta.json
@@ -0,0 +1,11 @@
+{
+ "owner": "mariokarras",
+ "slug": "abm-churn-prevention",
+ "displayName": "Churn Prevention",
+ "latest": {
+ "version": "1.0.0",
+ "publishedAt": 1773889209401,
+ "commit": "https://github.com/openclaw/skills/commit/402c5465bee5e890764eb4ed986781585d62d825"
+ },
+ "history": []
+}
diff --git a/skills/abm-churn-prevention/evals/evals.json b/skills/abm-churn-prevention/evals/evals.json
new file mode 100644
index 00000000..061b1a36
--- /dev/null
+++ b/skills/abm-churn-prevention/evals/evals.json
@@ -0,0 +1,93 @@
+{
+ "skill_name": "churn-prevention",
+ "evals": [
+ {
+ "id": 1,
+ "prompt": "Our SaaS product has a 7% monthly churn rate and we need to bring it down. We're a $49/month project management tool with about 2,000 paying customers. Can you help us design a churn prevention strategy?",
+ "expected_output": "Should check for product-marketing-context.md first. Should address both voluntary and involuntary churn. Should design a cancel flow following the framework: trigger → exit survey → dynamic save offer → confirmation → post-cancel nurture. Should include the 7 exit survey categories and recommend dynamic save offers mapped to each cancellation reason. Should address dunning for involuntary churn (pre-dunning, smart retry, email sequence, grace period). Should recommend a health score model. Should provide prioritized implementation plan.",
+ "assertions": [
+ "Checks for product-marketing-context.md",
+ "Addresses both voluntary and involuntary churn",
+ "Designs cancel flow with proper stages",
+ "Includes exit survey with multiple categories",
+ "Maps save offers to cancellation reasons",
+ "Addresses dunning stack for payment recovery",
+ "Recommends health score model",
+ "Provides prioritized implementation plan"
+ ],
+ "files": []
+ },
+ {
+ "id": 2,
+ "prompt": "We keep losing customers because their credit cards expire. About 15% of our churn is from failed payments. How do we fix this?",
+ "expected_output": "Should identify this as involuntary churn / payment recovery. Should apply the dunning stack framework: pre-dunning (card expiration reminders before failure), smart retry (retry logic based on failure reason), dunning email sequence (escalating urgency), grace period, and eventual cancellation. Should provide specific timing for each stage. Should recommend payment recovery tools and strategies (card updater services, backup payment methods). Should include recovery rate benchmarks.",
+ "assertions": [
+ "Identifies as involuntary churn / payment recovery",
+ "Applies dunning stack framework",
+ "Includes pre-dunning card expiration reminders",
+ "Includes smart retry logic",
+ "Provides dunning email sequence with escalating urgency",
+ "Recommends grace period before cancellation",
+ "Mentions card updater services or backup payment methods",
+ "Includes recovery benchmarks"
+ ],
+ "files": []
+ },
+ {
+ "id": 3,
+ "prompt": "what should we show users when they click the cancel button? right now they just go straight to cancellation with no attempt to save them",
+ "expected_output": "Should trigger on casual phrasing. Should design the cancel flow: cancel button → exit survey → dynamic save offer → confirmation → post-cancel. Should detail the exit survey categories (too expensive, missing feature, switched to competitor, not using enough, technical issues, bad support, other). Should provide dynamic save offers matched to each reason (e.g., too expensive → discount offer, missing feature → roadmap update, not using enough → onboarding help). Should include copy recommendations for each screen. Should warn against dark patterns (making it impossible to cancel).",
+ "assertions": [
+ "Triggers on casual phrasing",
+ "Designs multi-step cancel flow",
+ "Includes exit survey with 7 categories",
+ "Provides dynamic save offers mapped to reasons",
+ "Includes copy recommendations",
+ "Warns against dark patterns",
+ "Includes confirmation and post-cancel steps"
+ ],
+ "files": []
+ },
+ {
+ "id": 4,
+ "prompt": "How do we identify which customers are at risk of churning before they actually cancel? We want to be proactive.",
+ "expected_output": "Should apply the health score model framework. Should define health score components: product usage signals (login frequency, feature adoption, key action completion), engagement signals (support tickets, NPS responses, email engagement), and account signals (contract type, company growth, stakeholder changes). Should recommend scoring methodology (0-100 scale). Should define risk tiers and recommended interventions for each tier. Should suggest data sources and implementation approach.",
+ "assertions": [
+ "Applies health score model framework",
+ "Defines usage-based health signals",
+ "Defines engagement-based health signals",
+ "Defines account-based health signals",
+ "Recommends scoring methodology",
+ "Defines risk tiers with interventions",
+ "Suggests data sources and implementation"
+ ],
+ "files": []
+ },
+ {
+ "id": 5,
+ "prompt": "Our exit survey shows that 40% of cancellations say 'too expensive' as the reason. What save offers should we try?",
+ "expected_output": "Should reference the dynamic save offers mapped to the 'too expensive' reason. Should suggest multiple offer types: temporary discount, downgrade to cheaper plan, annual billing discount, pause instead of cancel, extended trial of current plan. Should recommend testing different offers to find what works best. Should also dig deeper — 'too expensive' often masks other issues (not seeing value, not using enough features). Should suggest follow-up questions in the exit survey to get more specific.",
+ "assertions": [
+ "References save offers for 'too expensive' reason",
+ "Suggests multiple offer types (discount, downgrade, pause)",
+ "Recommends testing different offers",
+ "Notes that 'too expensive' often masks other issues",
+ "Suggests deeper follow-up questions",
+ "Provides specific save offer copy or structure"
+ ],
+ "files": []
+ },
+ {
+ "id": 6,
+ "prompt": "We want to set up a win-back email sequence for customers who already cancelled. Can you help write those emails?",
+ "expected_output": "Should recognize this overlaps with email sequence work. Should defer to or cross-reference the email-sequence skill for writing the actual email sequence. May provide churn-specific context (timing post-cancel, re-engagement hooks, win-back offer strategy) but should make clear that email-sequence is the right skill for designing and writing the full email sequence.",
+ "assertions": [
+ "Recognizes overlap with email sequence work",
+ "References or defers to email-sequence skill",
+ "May provide churn-specific context for the sequence",
+ "Does not attempt to write a full email sequence"
+ ],
+ "files": []
+ }
+ ]
+}
diff --git a/skills/abm-churn-prevention/references/cancel-flow-patterns.md b/skills/abm-churn-prevention/references/cancel-flow-patterns.md
new file mode 100644
index 00000000..a47ab991
--- /dev/null
+++ b/skills/abm-churn-prevention/references/cancel-flow-patterns.md
@@ -0,0 +1,316 @@
+# Cancel Flow Patterns
+
+Detailed cancel flow patterns by business type, billing provider, and industry.
+
+---
+
+## Cancel Flow by Business Type
+
+### B2C / Self-Serve SaaS
+
+High volume, low touch. The flow must work without human intervention.
+
+**Flow structure:**
+```
+Cancel button → Exit survey (1 question) → Dynamic offer → Confirm → Post-cancel
+```
+
+**Characteristics:**
+- Fully automated, no human in the loop
+- Quick — 2-3 screens maximum
+- One offer + one fallback, not a menu of options
+- Mobile-optimized (significant cancellations on mobile)
+- Clear "continue cancelling" at every step
+
+**Typical save rate:** 20-30%
+
+**Example flow for a $29/mo productivity app:**
+1. "What's the main reason?" → 6 options
+2. Selected "Too expensive" → "Get 25% off for 3 months (save $21.75)"
+3. Declined → "Or switch to our Starter plan at $12/mo"
+4. Declined → "We're sorry to see you go. Your access continues until [date]."
+
+---
+
+### B2B / Team Plans
+
+Lower volume, higher stakes. Personal outreach is worth the cost.
+
+**Flow structure:**
+```
+Cancel button → Exit survey → Offer (or route to CS) → Confirm → Post-cancel
+```
+
+**Characteristics:**
+- Route accounts above MRR threshold to customer success
+- Show team impact ("Your 8 team members will lose access")
+- Offer admin-to-admin call for enterprise accounts
+- Longer consideration — allow "schedule a call" as a save option
+- Require admin/owner role to cancel (not any team member)
+
+**Typical save rate:** 30-45% (higher because of personal touch)
+
+**MRR-based routing:**
+
+| Account MRR | Cancel Flow |
+|-------------|-------------|
+| <$100/mo | Automated flow with offers |
+| $100-$500/mo | Automated + flag for CS follow-up |
+| $500-$2,000/mo | Route to CS before cancel completes |
+| $2,000+/mo | Block self-serve cancel, require CS call |
+
+---
+
+### Freemium / Free-to-Paid
+
+Users cancelling paid to return to free tier. Different psychology — they're not leaving, they're downgrading.
+
+**Flow structure:**
+```
+Cancel button → "Switch to Free?" prompt → Exit survey (if still cancelling) → Offer → Confirm
+```
+
+**Characteristics:**
+- Lead with the free tier as the first option (not a save offer)
+- Show what they keep on free vs. what they lose
+- The "save" is keeping them on free, not losing them entirely
+- Track free-tier users for future re-upgrade campaigns
+
+---
+
+## Cancel Flow by Billing Interval
+
+### Monthly Subscribers
+
+- More price-sensitive, shorter commitment
+- Discount offers work well (20-30% for 2-3 months)
+- Pause is effective (1-2 months)
+- Suggest annual plan at a discount as an alternative
+
+**Offer priority:**
+1. Discount (if reason = price)
+2. Pause (if reason = not using / temporary)
+3. Annual plan switch (if engaged but price-sensitive)
+
+### Annual Subscribers
+
+- Higher commitment, often cancelling for stronger reasons
+- Prorate refund expectations matter
+- Longer save window (they've already paid)
+- Personal outreach more justified (higher LTV at stake)
+
+**Offer priority:**
+1. Pause remainder of term (if temporary)
+2. Plan adjustment + credit for next renewal
+3. Personal outreach from CS
+4. Partial refund + downgrade (better than full refund + cancel)
+
+**Refund handling:**
+- Offer prorated refund if significant time remaining
+- "Pause until renewal" if less than 3 months left
+- Be generous — bad refund experiences create vocal detractors
+
+---
+
+## Save Offer Patterns
+
+### The Discount Ladder
+
+Don't lead with your biggest discount. Escalate:
+
+```
+Cancel click → 15% off → Still cancelling → 25% off → Still cancelling → Let them go
+```
+
+**Rules:**
+- Maximum 2 discount offers per cancel session
+- Never exceed 30% (higher trains cancel-for-discount behavior)
+- Time-limit discounts (2-3 months, then full price resumes)
+- Track discount accepters — if they cancel again at full price, don't re-offer
+
+### The Pause Playbook
+
+Pause is often better than a discount because it doesn't devalue your product.
+
+**Implementation:**
+
+| Setting | Recommendation |
+|---------|---------------|
+| Pause duration options | 1 month, 2 months, 3 months |
+| Default selection | 1 month (shortest) |
+| Maximum pause | 3 months (longer pauses rarely return) |
+| During pause | Keep data, remove access |
+| Reactivation | Auto-reactivate with 7-day advance email |
+| Repeat pauses | Allow 1 pause per 12-month period |
+
+**Pause reactivation sequence:**
+- Day -7: "Your pause ends in 7 days. We've been busy — here's what's new."
+- Day -1: "Welcome back tomorrow! Here's what's waiting for you."
+- Day 0: "You're back! Here's a quick tour of what's new."
+
+### The Downgrade Path
+
+For multi-plan products, downgrade is the strongest save:
+
+```
+┌─────────────────────────────────────────┐
+│ Before you go, what about right-sizing │
+│ your plan? │
+│ │
+│ Current: Pro ($49/mo) │
+│ │
+│ ┌─────────────────────────────────┐ │
+│ │ Switch to Starter ($19/mo) │ │
+│ │ │ │
+│ │ ✓ Keep: Projects, integrations │ │
+│ │ ✗ Lose: Advanced analytics, │ │
+│ │ team features │ │
+│ │ │ │
+│ │ [Switch to Starter] │ │
+│ └─────────────────────────────────┘ │
+│ │
+│ [No thanks, continue cancelling] │
+└─────────────────────────────────────────┘
+```
+
+**Downgrade best practices:**
+- Show exactly what they keep and what they lose
+- Use checkmarks and X marks for scanability
+- Preserve their data even on the lower plan
+- If they downgrade, don't show upgrade prompts for at least 30 days
+
+### The Competitor Switch Handler
+
+When the cancel reason is "switching to competitor":
+
+1. **Ask which competitor** (optional, don't force it)
+2. **Show a comparison** if you have one (see competitor-alternatives skill)
+3. **Offer a migration credit** ("We'll match their price for 3 months")
+4. **Request a feedback call** ("15 minutes to understand what we're missing")
+
+This data is gold for product and marketing teams.
+
+---
+
+## Post-Cancel Experience
+
+What happens after cancel matters for:
+- Win-back potential
+- Word of mouth
+- Review sentiment
+
+### Confirmation Page
+
+```
+Your subscription has been cancelled.
+
+What happens next:
+• Your access continues until [billing period end date]
+• Your data will be preserved for 90 days
+• You can reactivate anytime from your account settings
+
+[Reactivate My Account]
+
+We'd love to have you back. We'll keep improving based on feedback
+from customers like you.
+```
+
+### Post-Cancel Sequence
+
+| Timing | Action |
+|--------|--------|
+| Immediately | Confirmation email with access end date |
+| Day 1 | (Nothing — don't be desperate) |
+| Day 7 | NPS/satisfaction survey about overall experience |
+| Day 30 | "What's new" email with recent improvements |
+| Day 60 | Address their specific cancel reason if resolved |
+| Day 90 | Final win-back with special offer |
+
+**For detailed win-back email sequences**: See the email-sequence skill.
+
+---
+
+## Segmentation Rules
+
+The most effective cancel flows use segmentation to show different offers to different customers.
+
+### Segmentation Dimensions
+
+| Dimension | Why It Matters |
+|-----------|---------------|
+| Plan / MRR | Higher-value customers get personal outreach |
+| Tenure | Long-term customers get more generous offers |
+| Usage level | High-usage customers get different messaging than dormant ones |
+| Billing interval | Monthly vs. annual need different approaches |
+| Previous saves | Don't re-offer the same discount to a repeat canceller |
+| Cancel reason | Drives which offer to show (core mapping) |
+
+### Segment-Specific Flows
+
+**New customer (< 30 days):**
+- They haven't activated. The save is onboarding, not discounts.
+- Offer: Free onboarding call, setup help, extended trial
+- Ask: "What were you hoping to accomplish?" (learn what's missing)
+
+**Engaged customer cancelling on price:**
+- They love the product but can't justify the cost.
+- Offer: Discount, annual plan switch, downgrade
+- High save potential
+
+**Dormant customer (no login 30+ days):**
+- They forgot about you. A discount won't bring them back.
+- Offer: Pause subscription, "what changed?" conversation
+- Low save potential — focus on learning why
+
+**Power user switching to competitor:**
+- They're actively choosing something else.
+- Offer: Competitive match, feedback call, roadmap preview
+- Medium save potential — depends on reason
+
+---
+
+## Implementation Checklist
+
+### Phase 1: Foundation (Week 1)
+- [ ] Add cancel flow (survey + 1 offer + confirmation)
+- [ ] Set up exit survey with 5-7 reason categories
+- [ ] Map one offer per reason (simple 1:1 mapping)
+- [ ] Track cancel reasons and save rate in analytics
+- [ ] Enable pre-dunning card expiry emails
+
+### Phase 2: Optimization (Weeks 2-4)
+- [ ] Add fallback offers (primary + secondary per reason)
+- [ ] Implement pause subscription option
+- [ ] Set up dunning email sequence (4 emails over 10 days)
+- [ ] Enable smart retries (Stripe Smart Retries or equivalent)
+- [ ] Add MRR-based routing for high-value accounts
+
+### Phase 3: Advanced (Month 2+)
+- [ ] Build health score from usage signals
+- [ ] Set up proactive intervention triggers
+- [ ] A/B test discount amounts and offer types
+- [ ] Segment flows by plan, tenure, and usage
+- [ ] Post-cancel win-back sequence (coordinate with email-sequence skill)
+- [ ] Cohort analysis: churn by channel, plan, tenure
+
+---
+
+## Compliance Notes
+
+### FTC Click-to-Cancel Rule (US)
+- Cancellation must be as easy as signup
+- Cannot require a phone call to cancel if signup was online
+- Cannot add excessive steps to discourage cancellation
+- Save offers are allowed but "continue cancelling" must be clear
+
+### GDPR / Data Retention (EU)
+- Inform users about data retention period post-cancel
+- Offer data export before account deletion
+- Honor deletion requests within 30 days
+- Don't use post-cancel data for marketing without consent
+
+### General Best Practices
+- Always show a clear path to complete cancellation
+- Never hide the cancel button (dark pattern)
+- Process cancellation even if save flow has errors
+- Confirm cancellation with email receipt
diff --git a/skills/abm-churn-prevention/references/dunning-playbook.md b/skills/abm-churn-prevention/references/dunning-playbook.md
new file mode 100644
index 00000000..294e3b36
--- /dev/null
+++ b/skills/abm-churn-prevention/references/dunning-playbook.md
@@ -0,0 +1,408 @@
+# Dunning Playbook
+
+Complete guide to recovering failed payments and reducing involuntary churn.
+
+---
+
+## Why Dunning Matters
+
+- Failed payments cause 30-50% of all subscription churn
+- Most failed payments are recoverable with the right strategy
+- Subscription businesses lose an estimated $129 billion annually to involuntary churn
+- Effective dunning recovers 50-60% of failed payments
+
+---
+
+## The Dunning Timeline
+
+```
+Day -30 to -7: Pre-dunning (prevent failures)
+Day 0: Payment fails → Smart retry #1 + Email #1
+Day 1-3: Smart retry #2 + Email #2
+Day 3-5: Smart retry #3
+Day 5-7: Smart retry #4 + Email #3
+Day 7-10: Final retry + Email #4 (final warning)
+Day 10-14: Grace period ends → Account paused/cancelled
+Day 14+: Win-back sequence begins
+```
+
+---
+
+## Pre-Dunning: Prevent Failures Before They Happen
+
+### Card Expiry Management
+
+| Timing | Action |
+|--------|--------|
+| 30 days before expiry | Email: "Your card ending in 4242 expires next month" |
+| 15 days before expiry | Email: "Update your payment method to avoid interruption" |
+| 7 days before expiry | Email: "Your card expires in 7 days — update now" |
+| 3 days before expiry | In-app banner: "Payment method expiring soon" |
+
+**Email template — Card expiring:**
+```
+Subject: Your card ending in 4242 expires soon
+
+Hi [Name],
+
+The card on file for your [Product] subscription expires on [date].
+
+Update your payment method now to avoid any interruption:
+
+[Update Payment Method →]
+
+This takes less than 30 seconds.
+
+— [Product] Team
+```
+
+### Card Updater Services
+
+Major card networks offer automatic card update programs:
+
+| Service | Network | What It Does |
+|---------|---------|--------------|
+| Visa Account Updater (VAU) | Visa | Auto-updates stored card numbers and expiry dates |
+| Mastercard Automatic Billing Updater (ABU) | Mastercard | Same for Mastercard |
+| Amex Cardrefresher | American Express | Same for Amex |
+
+**Impact:** Reduces hard declines from expired/replaced cards by 30-50%.
+
+**How to enable:**
+- **Stripe**: Automatic — enabled by default
+- **Chargebee**: Enabled through gateway settings
+- **Recurly**: Built-in, enabled by default
+- **Braintree**: Contact processor to enable
+
+### Backup Payment Methods
+
+Prompt for a second payment method:
+- During signup: "Add a backup payment method" (low conversion)
+- After first successful payment: "Protect your account with a backup card" (better timing)
+- After a failed payment is recovered: "Add a backup to prevent future interruptions" (best timing — they felt the pain)
+
+### Pre-Billing Notifications
+
+For annual plans or high-value subscriptions:
+- Email 7 days before renewal with amount and date
+- Include link to update payment method
+- Show what's included in the renewal
+- Required by some regulations for auto-renewals
+
+---
+
+## Smart Retry Strategy
+
+### Decline Type Classification
+
+| Code | Type | Meaning | Retry? |
+|------|------|---------|--------|
+| `insufficient_funds` | Soft | Temporarily low balance | Yes — retry in 2-3 days |
+| `card_declined` (generic) | Soft | Various temporary reasons | Yes — retry 3-4 times |
+| `processing_error` | Soft | Gateway/network issue | Yes — retry within 24h |
+| `expired_card` | Hard | Card is expired | No — request new card |
+| `stolen_card` | Hard | Card reported stolen | No — request new card |
+| `do_not_honor` | Soft/Hard | Bank refused (ambiguous) | Try once more, then ask for new card |
+| `authentication_required` | Auth | SCA/3DS needed | Send customer to authenticate |
+
+### Retry Schedule by Provider
+
+**Stripe (Smart Retries — recommended):**
+- Enable "Smart Retries" in Stripe Dashboard → Billing → Settings
+- Stripe's ML model picks optimal retry timing based on billions of transactions
+- Typically 4-8 retry attempts over 3-4 weeks
+- Recovers ~15% more than fixed-schedule retries
+
+**Manual retry schedule (if no smart retries):**
+
+| Retry | Timing | Best Day/Time |
+|-------|--------|--------------|
+| 1 | Day 1 (24h after failure) | Morning, same day of week as original |
+| 2 | Day 3 | Try a different time of day |
+| 3 | Day 5 | After typical payday (1st, 15th) |
+| 4 | Day 7 | Morning of the next business day |
+| 5 (final) | Day 10 | Last attempt before grace period ends |
+
+**Retry timing insights:**
+- Retry on the same day of month the original payment succeeded
+- Retry after common paydays (1st and 15th of the month)
+- Avoid retrying on weekends (lower approval rates)
+- Morning retries (8-10am local time) perform slightly better
+
+---
+
+## Dunning Email Sequence
+
+### Email 1: Payment Failed (Day 0)
+
+**Tone:** Friendly, matter-of-fact. No alarm.
+
+```
+Subject: Action needed — your payment didn't go through
+
+Hi [Name],
+
+We tried to charge your [card type] ending in [last 4] for your
+[Product] subscription ($[amount]), but it didn't go through.
+
+This happens sometimes — usually a quick card update fixes it.
+
+[Update Payment Method →]
+
+Your access isn't affected yet. We'll retry automatically, but
+updating your card is the fastest fix.
+
+Need help? Just reply to this email.
+
+— [Product] Team
+```
+
+### Email 2: Reminder (Day 3)
+
+**Tone:** Helpful, slightly more urgent.
+
+```
+Subject: Quick reminder — update your payment for [Product]
+
+Hi [Name],
+
+Just a heads-up — we still haven't been able to process your
+$[amount] payment for [Product].
+
+[Update Payment Method →]
+
+Takes less than 30 seconds. Your [data/projects/team access]
+is safe, but we'll need a valid payment method to keep your
+account active.
+
+Questions? Reply here and we'll help.
+
+— [Product] Team
+```
+
+### Email 3: Urgency (Day 7)
+
+**Tone:** Direct, clear consequences.
+
+```
+Subject: Your [Product] account will be paused in 3 days
+
+Hi [Name],
+
+We've tried to process your payment several times, but your
+[card type] ending in [last 4] keeps getting declined.
+
+If we don't receive payment by [date], your account will be
+paused and you'll lose access to:
+
+• [Key feature/data they use]
+• [Their projects/workspace]
+• [Team access for X members]
+
+[Update Payment Method Now →]
+
+Your data won't be deleted — you can reactivate anytime by
+updating your payment method.
+
+— [Product] Team
+```
+
+### Email 4: Final Warning (Day 10)
+
+**Tone:** Final, clear, no guilt.
+
+```
+Subject: Last chance to keep your [Product] account active
+
+Hi [Name],
+
+This is our last reminder. Your payment of $[amount] is past
+due, and your account will be paused tomorrow ([date]).
+
+[Update Payment Method →]
+
+After pausing:
+• Your data is saved for [90 days]
+• You can reactivate anytime
+• Just update your card to restore access
+
+If you intended to cancel, no action needed — your account
+will be paused automatically.
+
+— [Product] Team
+```
+
+---
+
+## Grace Period Management
+
+### What Happens During Grace Period
+
+| Setting | Recommendation |
+|---------|---------------|
+| Duration | 7-14 days after final retry |
+| Access | Degraded (read-only) or full access |
+| Visibility | In-app banner: "Payment past due — update to continue" |
+| Retry | Continue background retries during grace |
+| Communication | Dunning emails continue |
+
+### Access Degradation Options
+
+**Option A: Full access during grace (recommended for B2B)**
+- Lower friction, customer feels respected
+- Higher recovery rate (they still see value)
+- Risk: some customers exploit the grace period
+
+**Option B: Read-only access (recommended for B2C)**
+- Can view but not create/edit
+- Creates urgency without data loss fear
+- Clear message: "Update payment to resume full access"
+
+**Option C: Immediate lockout (not recommended)**
+- Aggressive, damages relationship
+- Lower recovery rate
+- Only appropriate for very low-cost plans
+
+### Post-Grace Period
+
+| Timing | Action |
+|--------|--------|
+| Grace period ends | Pause account (not delete) |
+| Day 1 post-pause | "Your account has been paused" email |
+| Day 7 post-pause | "Your data is still here" reminder |
+| Day 30 post-pause | Win-back attempt with new offer |
+| Day 60 post-pause | Final win-back |
+| Day 90 post-pause | Data deletion warning (if applicable) |
+
+---
+
+## Provider-Specific Setup
+
+### Stripe
+
+**Enable Smart Retries:**
+1. Dashboard → Settings → Billing → Subscriptions and emails
+2. Enable "Smart Retries" under retry rules
+3. Set failed payment emails in Dashboard → Settings → Emails
+
+**Custom retry rules (if not using Smart Retries):**
+```
+Retry 1: 3 days after failure
+Retry 2: 5 days after failure
+Retry 3: 7 days after failure
+Final: Mark subscription as unpaid after last retry
+```
+
+**Webhook events to handle:**
+- `invoice.payment_failed` — trigger dunning
+- `invoice.paid` — cancel dunning, restore access
+- `customer.subscription.updated` — status changes
+- `customer.subscription.deleted` — final cancellation
+
+### Chargebee
+
+**Built-in dunning:**
+1. Settings → Configure Chargebee → Retry Settings
+2. Configure retry attempts and intervals
+3. Settings → Configure Chargebee → Email Notifications → Dunning
+
+**Dunning options:**
+- Automatic retries with configurable schedule
+- Built-in dunning emails (customizable templates)
+- Grace period configuration per plan
+
+### Paddle
+
+**Managed dunning:**
+- Paddle handles retries and dunning automatically
+- Limited customization (Paddle manages the relationship)
+- Webhook: `subscription.payment_failed`, `subscription.cancelled`
+- Best for hands-off approach
+
+### Recurly
+
+**Revenue Recovery:**
+1. Configuration → Dunning Management
+2. Set retry schedule per plan
+3. Configure grace period and final action (pause vs cancel)
+
+**Advanced features:**
+- Machine-learning retry optimization
+- Per-plan dunning schedules
+- Built-in Account Updater
+
+---
+
+## In-App Dunning
+
+Don't rely on email alone. Show payment failures in the app:
+
+### Banner Pattern
+```
+┌──────────────────────────────────────────────────────┐
+│ ⚠ Your payment of $29 failed. Update your card to │
+│ avoid losing access. [Update Payment →] [Dismiss] │
+└──────────────────────────────────────────────────────┘
+```
+
+**Rules:**
+- Show on every page load during dunning period
+- Allow dismiss (but show again next session)
+- Direct link to payment update (fewest clicks possible)
+- Don't block the product — let them continue using it
+
+### Modal Pattern (for final warning)
+```
+┌─────────────────────────────────────┐
+│ │
+│ Your account will be paused │
+│ on [date] │
+│ │
+│ Update your payment method to │
+│ keep access to your [X] projects │
+│ and [Y] team members. │
+│ │
+│ [Update Payment Method] │
+│ [Remind Me Later] │
+│ │
+└─────────────────────────────────────┘
+```
+
+---
+
+## Measuring Dunning Performance
+
+### Key Metrics
+
+| Metric | How to Calculate | Target |
+|--------|-----------------|--------|
+| Recovery rate | Recovered payments / Total failed | 50-60% |
+| Recovery rate by decline type | Recovered / Failed per type | Soft: 70%+, Hard: 40%+ |
+| Time to recovery | Days from failure to successful payment | <5 days |
+| Pre-dunning prevention rate | Prevented failures / Expected failures | 20-30% |
+| Dunning email open rate | Opens / Sent per email | 60%+ |
+| Dunning email click rate | Clicks / Opens per email | 30%+ |
+| Revenue recovered (monthly) | Sum of recovered payment amounts | Track trend |
+| Revenue lost to involuntary churn | Sum of failed + unrecovered amounts | Track trend |
+
+### Benchmarking
+
+**By company stage:**
+
+| Stage | Typical Involuntary Churn | Target After Optimization |
+|-------|--------------------------|--------------------------|
+| Early (< $1M ARR) | 3-5% of MRR/month | 1-2% |
+| Growth ($1-10M ARR) | 2-4% of MRR/month | 0.5-1.5% |
+| Scale ($10M+ ARR) | 1-3% of MRR/month | 0.3-0.8% |
+
+### ROI Calculation
+
+```
+Monthly failed payment MRR: $10,000
+Current recovery rate: 30% ($3,000 recovered)
+Target recovery rate: 60% ($6,000 recovered)
+Monthly improvement: $3,000/month
+Annual improvement: $36,000/year
+Cost of dunning optimization: ~$200-500/month (tooling)
+ROI: 6-15x
+```
diff --git a/skills/add-analytics/SKILL.md b/skills/add-analytics/SKILL.md
new file mode 100644
index 00000000..87e52f93
--- /dev/null
+++ b/skills/add-analytics/SKILL.md
@@ -0,0 +1,771 @@
+---
+name: add-analytics
+description: Add Google Analytics 4 tracking to any project. Detects framework, adds tracking code, sets up events, and configures privacy settings.
+argument-hint: " [--events] [--consent] [--debug]"
+---
+
+# Google Analytics 4 Setup Skill
+
+You are setting up Google Analytics 4 (GA4) for a project. Follow this comprehensive guide to add analytics properly.
+
+## Arguments
+
+Parse the following from `$ARGUMENTS`:
+- **Measurement ID**: Format `G-XXXXXXXXXX` (required, ask if not provided)
+- **--events**: Include custom event tracking helpers
+- **--consent**: Include cookie consent integration
+- **--debug**: Enable debug mode for development
+
+## Step 1: Detect Project Type
+
+Scan the project to determine the framework/setup:
+
+```
+Priority detection order:
+1. next.config.js/ts → Next.js
+2. nuxt.config.js/ts → Nuxt.js
+3. astro.config.mjs → Astro
+4. svelte.config.js → SvelteKit
+5. remix.config.js → Remix
+6. gatsby-config.js → Gatsby
+7. vite.config.js + src/App.vue → Vue + Vite
+8. vite.config.js + src/App.tsx → React + Vite
+9. angular.json → Angular
+10. package.json with "react-scripts" → Create React App
+11. index.html only → Plain HTML
+12. _app.tsx/jsx → Next.js (App Router check: app/ directory)
+```
+
+Also check for:
+- TypeScript usage (tsconfig.json)
+- Existing analytics (search for gtag, GA, analytics)
+- Package manager (pnpm-lock.yaml, yarn.lock, package-lock.json)
+
+## Step 2: Validate Measurement ID
+
+The Measurement ID must:
+- Start with `G-` (GA4 format)
+- Be followed by exactly 10 alphanumeric characters
+- Example: `G-ABC1234567`
+
+If the user provides a `UA-` ID, inform them:
+> "You provided a Universal Analytics ID (UA-). GA4 uses Measurement IDs starting with 'G-'.
+> Universal Analytics was sunset in July 2024. You'll need to create a GA4 property at analytics.google.com"
+
+## Step 3: Implementation by Framework
+
+### Next.js (App Router - app/ directory)
+
+Create `app/layout.tsx` modification or create `components/GoogleAnalytics.tsx`:
+
+```tsx
+// components/GoogleAnalytics.tsx
+'use client'
+
+import Script from 'next/script'
+
+interface GoogleAnalyticsProps {
+ measurementId: string
+}
+
+export function GoogleAnalytics({ measurementId }: GoogleAnalyticsProps) {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+Add to root layout:
+```tsx
+// app/layout.tsx
+import { GoogleAnalytics } from '@/components/GoogleAnalytics'
+
+// Add inside or :
+
+```
+
+### Next.js (Pages Router - pages/ directory)
+
+Modify `pages/_app.tsx`:
+
+```tsx
+// pages/_app.tsx
+import type { AppProps } from 'next/app'
+import Script from 'next/script'
+
+const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
+
+export default function App({ Component, pageProps }: AppProps) {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+```
+
+### React (Vite/CRA)
+
+Create `src/lib/analytics.ts`:
+
+```typescript
+// src/lib/analytics.ts
+export const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID
+
+declare global {
+ interface Window {
+ gtag: (...args: unknown[]) => void
+ dataLayer: unknown[]
+ }
+}
+
+export const initGA = () => {
+ if (typeof window === 'undefined') return
+
+ const script = document.createElement('script')
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`
+ script.async = true
+ document.head.appendChild(script)
+
+ window.dataLayer = window.dataLayer || []
+ window.gtag = function gtag() {
+ window.dataLayer.push(arguments)
+ }
+ window.gtag('js', new Date())
+ window.gtag('config', GA_MEASUREMENT_ID)
+}
+
+export const pageview = (url: string) => {
+ window.gtag('config', GA_MEASUREMENT_ID, {
+ page_path: url,
+ })
+}
+
+export const event = (action: string, params?: Record) => {
+ window.gtag('event', action, params)
+}
+```
+
+Initialize in `src/main.tsx`:
+
+```tsx
+import { initGA } from './lib/analytics'
+
+// Initialize before render
+if (import.meta.env.PROD) {
+ initGA()
+}
+```
+
+### Vue 3 (Vite)
+
+Create `src/plugins/analytics.ts`:
+
+```typescript
+// src/plugins/analytics.ts
+import type { App } from 'vue'
+import type { Router } from 'vue-router'
+
+const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID
+
+declare global {
+ interface Window {
+ gtag: (...args: unknown[]) => void
+ dataLayer: unknown[]
+ }
+}
+
+export const analyticsPlugin = {
+ install(app: App, { router }: { router: Router }) {
+ // Load gtag script
+ const script = document.createElement('script')
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`
+ script.async = true
+ document.head.appendChild(script)
+
+ window.dataLayer = window.dataLayer || []
+ window.gtag = function gtag() {
+ window.dataLayer.push(arguments)
+ }
+ window.gtag('js', new Date())
+ window.gtag('config', GA_MEASUREMENT_ID)
+
+ // Track route changes
+ router.afterEach((to) => {
+ window.gtag('config', GA_MEASUREMENT_ID, {
+ page_path: to.fullPath,
+ })
+ })
+
+ // Provide global methods
+ app.config.globalProperties.$gtag = window.gtag
+ }
+}
+```
+
+### Nuxt 3
+
+Create `plugins/analytics.client.ts`:
+
+```typescript
+// plugins/analytics.client.ts
+export default defineNuxtPlugin(() => {
+ const config = useRuntimeConfig()
+ const measurementId = config.public.gaMeasurementId
+
+ if (!measurementId) return
+
+ // Load gtag
+ useHead({
+ script: [
+ {
+ src: `https://www.googletagmanager.com/gtag/js?id=${measurementId}`,
+ async: true,
+ },
+ {
+ innerHTML: `
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', '${measurementId}');
+ `,
+ },
+ ],
+ })
+
+ // Track route changes
+ const router = useRouter()
+ router.afterEach((to) => {
+ window.gtag('config', measurementId, {
+ page_path: to.fullPath,
+ })
+ })
+})
+```
+
+Add to `nuxt.config.ts`:
+
+```typescript
+export default defineNuxtConfig({
+ runtimeConfig: {
+ public: {
+ gaMeasurementId: process.env.NUXT_PUBLIC_GA_MEASUREMENT_ID,
+ },
+ },
+})
+```
+
+### Astro
+
+Create `src/components/Analytics.astro`:
+
+```astro
+---
+// src/components/Analytics.astro
+interface Props {
+ measurementId: string
+}
+
+const { measurementId } = Astro.props
+---
+
+
+
+
+```
+
+Add to layout:
+
+```astro
+---
+import Analytics from '../components/Analytics.astro'
+---
+
+
+
+
+
+```
+
+### SvelteKit
+
+Create `src/lib/analytics.ts` and `src/routes/+layout.svelte`:
+
+```typescript
+// src/lib/analytics.ts
+import { browser } from '$app/environment'
+
+export const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID
+
+export function initGA() {
+ if (!browser) return
+
+ const script = document.createElement('script')
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`
+ script.async = true
+ document.head.appendChild(script)
+
+ window.dataLayer = window.dataLayer || []
+ window.gtag = function gtag() {
+ window.dataLayer.push(arguments)
+ }
+ window.gtag('js', new Date())
+ window.gtag('config', GA_MEASUREMENT_ID)
+}
+
+export function trackPageview(url: string) {
+ if (!browser) return
+ window.gtag('config', GA_MEASUREMENT_ID, { page_path: url })
+}
+```
+
+```svelte
+
+
+
+
+```
+
+### Plain HTML
+
+Add to ``:
+
+```html
+
+
+
+```
+
+## Step 4: Environment Variables
+
+Create or update `.env` / `.env.local`:
+
+```bash
+# For Next.js
+NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
+
+# For Vite (React/Vue/Svelte)
+VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
+
+# For Nuxt
+NUXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
+```
+
+Add to `.env.example` if it exists (without the actual ID):
+
+```bash
+# Google Analytics 4 Measurement ID
+NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
+```
+
+**IMPORTANT**: Add `.env.local` to `.gitignore` if not already present.
+
+## Step 5: Event Tracking Helpers (if --events flag)
+
+Create a comprehensive events utility:
+
+```typescript
+// lib/analytics-events.ts
+
+/**
+ * GA4 Event Tracking Utilities
+ *
+ * Recommended events: https://support.google.com/analytics/answer/9267735
+ */
+
+type GTagEvent = {
+ action: string
+ category?: string
+ label?: string
+ value?: number
+ [key: string]: unknown
+}
+
+// Core event function
+export const trackEvent = ({ action, category, label, value, ...rest }: GTagEvent) => {
+ if (typeof window === 'undefined' || !window.gtag) return
+
+ window.gtag('event', action, {
+ event_category: category,
+ event_label: label,
+ value,
+ ...rest,
+ })
+}
+
+// Engagement events
+export const trackClick = (elementName: string, location?: string) => {
+ trackEvent({
+ action: 'click',
+ category: 'engagement',
+ label: elementName,
+ click_location: location,
+ })
+}
+
+export const trackScroll = (percentage: number) => {
+ trackEvent({
+ action: 'scroll',
+ category: 'engagement',
+ value: percentage,
+ })
+}
+
+// Conversion events
+export const trackSignUp = (method: string) => {
+ trackEvent({
+ action: 'sign_up',
+ method,
+ })
+}
+
+export const trackLogin = (method: string) => {
+ trackEvent({
+ action: 'login',
+ method,
+ })
+}
+
+export const trackPurchase = (params: {
+ transactionId: string
+ value: number
+ currency: string
+ items?: Array<{
+ itemId: string
+ itemName: string
+ price: number
+ quantity: number
+ }>
+}) => {
+ trackEvent({
+ action: 'purchase',
+ transaction_id: params.transactionId,
+ value: params.value,
+ currency: params.currency,
+ items: params.items,
+ })
+}
+
+// Content events
+export const trackSearch = (searchTerm: string) => {
+ trackEvent({
+ action: 'search',
+ search_term: searchTerm,
+ })
+}
+
+export const trackShare = (method: string, contentType: string, itemId: string) => {
+ trackEvent({
+ action: 'share',
+ method,
+ content_type: contentType,
+ item_id: itemId,
+ })
+}
+
+// Form events
+export const trackFormStart = (formName: string) => {
+ trackEvent({
+ action: 'form_start',
+ form_name: formName,
+ })
+}
+
+export const trackFormSubmit = (formName: string, success: boolean) => {
+ trackEvent({
+ action: 'form_submit',
+ form_name: formName,
+ success,
+ })
+}
+
+// Error tracking
+export const trackError = (errorMessage: string, errorLocation?: string) => {
+ trackEvent({
+ action: 'exception',
+ description: errorMessage,
+ fatal: false,
+ error_location: errorLocation,
+ })
+}
+
+// Custom event builder for flexibility
+export const createCustomEvent = (eventName: string) => {
+ return (params?: Record) => {
+ trackEvent({
+ action: eventName,
+ ...params,
+ })
+ }
+}
+```
+
+## Step 6: Cookie Consent Integration (if --consent flag)
+
+Create a consent-aware wrapper:
+
+```typescript
+// lib/analytics-consent.ts
+
+type ConsentState = 'granted' | 'denied'
+
+interface ConsentConfig {
+ analytics_storage: ConsentState
+ ad_storage: ConsentState
+ ad_user_data: ConsentState
+ ad_personalization: ConsentState
+}
+
+const CONSENT_COOKIE = 'analytics_consent'
+
+// Initialize with consent mode
+export const initWithConsent = (measurementId: string) => {
+ if (typeof window === 'undefined') return
+
+ // Set default consent state (denied until user consents)
+ window.gtag('consent', 'default', {
+ analytics_storage: 'denied',
+ ad_storage: 'denied',
+ ad_user_data: 'denied',
+ ad_personalization: 'denied',
+ wait_for_update: 500, // Wait for consent banner
+ })
+
+ // Load gtag
+ const script = document.createElement('script')
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`
+ script.async = true
+ document.head.appendChild(script)
+
+ window.dataLayer = window.dataLayer || []
+ window.gtag = function gtag() {
+ window.dataLayer.push(arguments)
+ }
+ window.gtag('js', new Date())
+ window.gtag('config', measurementId)
+
+ // Check for existing consent
+ const savedConsent = getCookie(CONSENT_COOKIE)
+ if (savedConsent) {
+ updateConsent(JSON.parse(savedConsent))
+ }
+}
+
+// Update consent when user makes a choice
+export const updateConsent = (consent: Partial) => {
+ if (typeof window === 'undefined' || !window.gtag) return
+
+ const consentState: ConsentConfig = {
+ analytics_storage: consent.analytics_storage || 'denied',
+ ad_storage: consent.ad_storage || 'denied',
+ ad_user_data: consent.ad_user_data || 'denied',
+ ad_personalization: consent.ad_personalization || 'denied',
+ }
+
+ window.gtag('consent', 'update', consentState)
+
+ // Save to cookie
+ setCookie(CONSENT_COOKIE, JSON.stringify(consentState), 365)
+}
+
+// Convenience functions
+export const acceptAll = () => {
+ updateConsent({
+ analytics_storage: 'granted',
+ ad_storage: 'granted',
+ ad_user_data: 'granted',
+ ad_personalization: 'granted',
+ })
+}
+
+export const acceptAnalyticsOnly = () => {
+ updateConsent({
+ analytics_storage: 'granted',
+ ad_storage: 'denied',
+ ad_user_data: 'denied',
+ ad_personalization: 'denied',
+ })
+}
+
+export const denyAll = () => {
+ updateConsent({
+ analytics_storage: 'denied',
+ ad_storage: 'denied',
+ ad_user_data: 'denied',
+ ad_personalization: 'denied',
+ })
+}
+
+// Cookie utilities
+function setCookie(name: string, value: string, days: number) {
+ const date = new Date()
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
+ document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;SameSite=Lax`
+}
+
+function getCookie(name: string): string | null {
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))
+ return match ? match[2] : null
+}
+```
+
+## Step 7: Debug Mode (if --debug flag)
+
+Add debug configuration:
+
+```typescript
+// For development, enable debug mode
+if (process.env.NODE_ENV === 'development') {
+ window.gtag('config', 'G-XXXXXXXXXX', {
+ debug_mode: true,
+ })
+}
+```
+
+Also recommend installing the [Google Analytics Debugger](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) Chrome extension.
+
+## Step 8: TypeScript Declarations
+
+Create `types/gtag.d.ts` if using TypeScript:
+
+```typescript
+// types/gtag.d.ts
+declare global {
+ interface Window {
+ gtag: Gtag.Gtag
+ dataLayer: object[]
+ }
+}
+
+declare namespace Gtag {
+ interface Gtag {
+ (command: 'config', targetId: string, config?: ConfigParams): void
+ (command: 'set', targetId: string, config: ConfigParams): void
+ (command: 'set', config: ConfigParams): void
+ (command: 'js', date: Date): void
+ (command: 'event', eventName: string, eventParams?: EventParams): void
+ (command: 'consent', consentArg: 'default' | 'update', consentParams: ConsentParams): void
+ (...args: unknown[]): void
+ }
+
+ interface ConfigParams {
+ page_title?: string
+ page_location?: string
+ page_path?: string
+ send_page_view?: boolean
+ debug_mode?: boolean
+ [key: string]: unknown
+ }
+
+ interface EventParams {
+ event_category?: string
+ event_label?: string
+ value?: number
+ [key: string]: unknown
+ }
+
+ interface ConsentParams {
+ analytics_storage?: 'granted' | 'denied'
+ ad_storage?: 'granted' | 'denied'
+ ad_user_data?: 'granted' | 'denied'
+ ad_personalization?: 'granted' | 'denied'
+ wait_for_update?: number
+ }
+}
+
+export {}
+```
+
+## Step 9: Verification Checklist
+
+After implementation, verify:
+
+1. [ ] Measurement ID is correct format (G-XXXXXXXXXX)
+2. [ ] Script loads in production (check Network tab)
+3. [ ] Real-time reports show activity in GA4 dashboard
+4. [ ] Page views are tracked on navigation
+5. [ ] No console errors related to gtag
+6. [ ] Environment variables are not committed to git
+7. [ ] TypeScript has no type errors (if applicable)
+
+## Step 10: Summary Output
+
+After completing setup, provide the user with:
+
+1. **Files created/modified** (list them)
+2. **Environment variables needed** (with example values)
+3. **Next steps**:
+ - Add the Measurement ID to environment variables
+ - Deploy and verify in GA4 Real-time reports
+ - Set up conversions in GA4 dashboard
+ - Consider adding custom events for key user actions
+
+## Common Issues & Solutions
+
+**"gtag is not defined"**
+- Script hasn't loaded yet; ensure async loading is handled
+
+**No data in GA4**
+- Check if ad blockers are preventing tracking
+- Verify Measurement ID is correct
+- Check browser console for errors
+
+**Double page views**
+- SPA router sending duplicate events; implement deduplication
+
+**GDPR Compliance**
+- Always implement consent mode for EU users
+- Use the --consent flag to add consent management
diff --git a/skills/add-analytics/_meta.json b/skills/add-analytics/_meta.json
new file mode 100644
index 00000000..d94c1a45
--- /dev/null
+++ b/skills/add-analytics/_meta.json
@@ -0,0 +1,11 @@
+{
+ "owner": "jeftekhari",
+ "slug": "add-analytics",
+ "displayName": "Add Analytics",
+ "latest": {
+ "version": "0.1.0",
+ "publishedAt": 1770054052971,
+ "commit": "https://github.com/clawdbot/skills/commit/8f2de9cbb33f8e3ed1b3e389e6ad183975cda1f8"
+ },
+ "history": []
+}
diff --git a/skills/admapix-ice/README.md b/skills/admapix-ice/README.md
new file mode 100644
index 00000000..2cc41c86
--- /dev/null
+++ b/skills/admapix-ice/README.md
@@ -0,0 +1,55 @@
+# AdMapix — Ad Intelligence & App Analytics Skill
+
+[中文文档](README_CN.md)
+
+All-in-one ad intelligence assistant. Search ad creatives, analyze apps, explore rankings, track downloads/revenue, and get market insights — all through natural language.
+
+## Features
+
+- **Creative Search** — Search ad creatives by keyword, region, media, creative type, with H5 visual results
+- **App Analysis** — Look up any app's details, developer info, and ad creative portfolio
+- **Rankings** — App Store / Google Play charts, promotion rankings, download rankings, revenue rankings
+- **Download & Revenue** — Track download and revenue trends over time (third-party estimates)
+- **Ad Distribution** — Analyze where and how an app advertises (countries, media placements, creative formats)
+- **Market Analysis** — Industry-level insights by country, media channel, advertiser, and publisher
+- **Deep Dive** — Multi-dimensional reports combining all of the above
+
+## Install
+
+```bash
+npx clawhub install admapix
+```
+
+## Setup
+
+1. Go to [www.admapix.com](https://www.admapix.com) to register and get your API Key
+2. Configure:
+
+```bash
+openclaw config set skills.entries.admapix.apiKey "YOUR_ADMAPIX_API_KEY"
+```
+
+## Usage Examples
+
+After setup, just tell your AI assistant:
+
+| Category | Example prompts |
+|----------|----------------|
+| Creative Search | "Search video ads for puzzle games", "Find casual game creatives in Southeast Asia" |
+| App Analysis | "Tell me about Temu", "Who is the developer of TikTok?" |
+| Rankings | "App Store free chart US", "Top apps by ad spend this week" |
+| Downloads | "How are Temu's downloads trending?", "Compare Temu vs SHEIN downloads" |
+| Ad Distribution | "Which countries does Temu advertise in?", "What ad channels does this game use?" |
+| Market Analysis | "Which country has the most game ads?", "Who are the top game advertisers?" |
+| Deep Dive | "Full ad strategy analysis for Temu", "Compare Temu and SHEIN" |
+
+Supports both **English** and **Chinese** — the assistant responds in your language.
+
+## Links
+
+- Website: [www.admapix.com](https://www.admapix.com)
+- GitHub: [github.com/fly0pants/admapix](https://github.com/fly0pants/admapix)
+
+---
+
+Built by [Miaozhisheng](https://www.admapix.com)
diff --git a/skills/admapix-ice/README_CN.md b/skills/admapix-ice/README_CN.md
new file mode 100644
index 00000000..f9c7c8fb
--- /dev/null
+++ b/skills/admapix-ice/README_CN.md
@@ -0,0 +1,55 @@
+# AdMapix — 广告情报与应用分析 Skill
+
+[English](README.md)
+
+一站式广告情报助手。通过自然语言搜索广告素材、分析应用、查看排行榜、追踪下载量/收入、获取市场洞察。
+
+## 功能
+
+- **素材搜索** — 按关键词、地区、媒体、素材类型搜索广告创意,支持 H5 可视化结果
+- **应用分析** — 查询任意应用的详情、开发者信息、广告素材库
+- **排行榜** — App Store / Google Play 官方榜单,推广排行、下载排行、收入排行
+- **下载量与收入** — 追踪下载量和收入的时间趋势(第三方估算数据)
+- **投放分布** — 分析应用在哪些国家、哪些媒体位、用什么素材类型投放广告
+- **市场分析** — 按国家、媒体渠道、广告主、流量主维度的行业级洞察
+- **深度分析** — 多维度综合报告,整合以上所有能力
+
+## 安装
+
+```bash
+npx clawhub install admapix
+```
+
+## 配置
+
+1. 前往 [www.admapix.com](https://www.admapix.com) 注册并获取 API Key
+2. 配置环境变量:
+
+```bash
+openclaw config set skills.entries.admapix.apiKey "你的ADMAPIX_API_KEY"
+```
+
+## 使用示例
+
+安装配置完成后,直接对 AI 助手说:
+
+| 分类 | 示例指令 |
+|------|----------|
+| 素材搜索 | 「搜一下 puzzle game 的视频广告」「找东南亚投放的休闲游戏素材」 |
+| 应用分析 | 「分析一下 Temu」「TikTok 的开发者是谁?」 |
+| 排行榜 | 「美国 App Store 免费榜」「这周广告投放量最大的 App」 |
+| 下载量 | 「Temu 最近下载量怎么样?」「对比 Temu 和 SHEIN 的下载量」 |
+| 投放分布 | 「Temu 主要在哪些国家投广告?」「这个游戏用了哪些广告渠道?」 |
+| 市场分析 | 「全球游戏广告市场哪个国家最大?」「谁是最大的游戏广告主?」 |
+| 深度分析 | 「全面分析 Temu 的广告策略」「对比 Temu 和 SHEIN」 |
+
+支持 **中文** 和 **英文** 双语 — 助手会自动匹配你的语言。
+
+## 链接
+
+- 官网:[www.admapix.com](https://www.admapix.com)
+- GitHub:[github.com/fly0pants/admapix](https://github.com/fly0pants/admapix)
+
+---
+
+由 [妙智盛](https://www.admapix.com) 提供技术支持
diff --git a/skills/admapix-ice/SKILL.md b/skills/admapix-ice/SKILL.md
new file mode 100644
index 00000000..0adf7a10
--- /dev/null
+++ b/skills/admapix-ice/SKILL.md
@@ -0,0 +1,394 @@
+---
+name: admapix
+description: "Ad intelligence & app analytics assistant. Search ad creatives, analyze apps, view rankings, track downloads/revenue, and get market insights via api.admapix.com. Triggers: 找素材, 搜广告, 广告素材, 竞品分析, 广告分析, 排行榜, 下载量, 收入分析, 市场分析, 投放分析, App分析, 出海分析, search ads, find creatives, ad spy, ad analysis, app ranking, download data, revenue, market analysis, app intelligence, competitor analysis, ad distribution."
+metadata: {"openclaw":{"emoji":"🎯","primaryEnv":"ADMAPIX_API_KEY"}}
+---
+
+# AdMapix Intelligence Assistant
+
+You are an ad intelligence and app analytics assistant. Help users search ad creatives, analyze apps, explore rankings, track downloads/revenue, and understand market trends — all via the AdMapix API.
+
+**Data disclaimer:** Download/revenue figures are third-party estimates, not official data. Always note this when presenting such data.
+
+## Language Handling / 语言适配
+
+Detect the user's language from their **first message** and maintain it throughout the conversation.
+
+| User language | Response language | Number format | H5 keyword | Example output |
+|---|---|---|---|---|
+| 中文 | 中文 | 万/亿 (e.g. 1.2亿) | Use Chinese keyword if possible | "共找到 1,234 条素材" |
+| English | English | K/M/B (e.g. 120M) | Use English keyword | "Found 1,234 creatives" |
+
+**Rules:**
+1. **All text output** (summaries, analysis, table headers, insights, follow-up hints) must match the detected language.
+2. **H5 page generation:** When using `generate_page: true`, pass the keyword in the user's language so the generated page displays in the matching language context.
+3. **Field name presentation:**
+ - Chinese → use Chinese labels: 应用名称, 开发者, 曝光量, 投放天数, 素材类型
+ - English → use English labels: App Name, Developer, Impressions, Active Days, Creative Type
+4. **Error messages** must also match: "未找到数据" vs "No data found".
+5. **Data disclaimers:** "⚠️ 下载量和收入为第三方估算数据" vs "⚠️ Download and revenue figures are third-party estimates."
+6. If the user **switches language mid-conversation**, follow the new language from that point on.
+
+## API Access
+
+Base URL: `https://api.admapix.com`
+Auth header: `X-API-Key: $ADMAPIX_API_KEY`
+
+All endpoints use this pattern:
+
+```bash
+# GET
+curl -s "https://api.admapix.com/api/data/{endpoint}?{params}" \
+ -H "X-API-Key: $ADMAPIX_API_KEY"
+
+# POST
+curl -s -X POST "https://api.admapix.com/api/data/{endpoint}" \
+ -H "X-API-Key: $ADMAPIX_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{...}'
+```
+
+## Interaction Flow
+
+### Step 1: Check API Key
+
+Before any query, run: `[ -n "$ADMAPIX_API_KEY" ] && echo "ok" || echo "missing"`
+
+**Never print the key value.** If missing, output:
+
+```
+🔑 You need an AdMapix API Key.
+
+1. Go to https://www.admapix.com to register
+2. Configure: openclaw config set skills.entries.admapix.apiKey "YOUR_KEY"
+3. Try again 🎉
+```
+
+### Step 1.5: Complexity Classification — 复杂度分类
+
+Before routing, classify the query complexity to decide the execution path:
+
+| Complexity | Criteria | Path | Examples |
+|---|---|---|---|
+| **Simple** | Can be answered with exactly 1 API call; single-entity, single-metric lookup | Skill handles directly (Step 2 onward) | "Temu排名第几", "搜一下休闲游戏素材", "Temu下载量", "Top 10 游戏" |
+| **Deep** | Requires 2+ API calls, any cross-entity/cross-dimensional query, analysis, comparison, or trend interpretation | Route to Deep Research Framework | "分析Temu的广告投放策略", "Temu和Shein对比", "放置少女的投放策略和竞品对比", "东南亚手游市场分析" |
+
+**Classification rule — count the API calls needed:**
+
+Simple (exactly 1 API call):
+- Single search: "搜一下休闲游戏素材" → 1× search
+- Single ranking: "iOS免费榜Top10" → 1× store-rank
+- Single detail: "Temu的开发者是谁" → 1× unified-product-search
+- Single metric: "Temu下载量" → 1× download-detail (after getting ID, but that's lookup+query=2, so actually **Deep**)
+
+Deep (2+ API calls):
+- Any query requiring entity lookup + data fetch: "Temu下载量" needs search→download = 2 calls → **Deep**
+- Any analysis: "分析XX" → always multi-call → **Deep**
+- Any comparison: "对比XX和YY" → always multi-call → **Deep**
+- Any market overview: "XX市场分析" → always multi-call → **Deep**
+- Any trend: "XX趋势" → always multi-call → **Deep**
+
+**In practice, only these are Simple:**
+- Direct keyword search with no analysis: "搜XX素材", "找XX广告"
+- Direct ranking with no drill-down: "排行榜", "Top 10"
+- Filter-options or param lookups
+
+**Default:** If unsure, classify as **Deep** (prefer thorough over incomplete).
+
+**Execution paths:**
+
+**→ Simple path:** Continue to Step 2 (existing routing logic). At the end of the response, append a hint in the user's language:
+- Chinese: `💡 需要更深入的分析?试试说"深度分析{topic}"`
+- English: `💡 Want deeper analysis? Try "deep research on {topic}"`
+
+**→ Deep path:** Call the Deep Research Framework.
+
+This is a 3-step process. Do NOT use `[[reply_to_current]]` until Step 3.
+
+**Step 1 — Submit the research task (returns instantly):**
+
+Run this exact command (only replace `{user_query}` and `{additional_context}`):
+```bash
+curl -s -X POST "https://deepresearch.admapix.com/research" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer test-local-token-2026" \
+ -d '{"project": "admapix", "query": "{user_query}", "context": "{additional_context}", "api_key": "'"$ADMAPIX_API_KEY"'"}'
+```
+
+- `project` is always `"admapix"` — do NOT change this.
+- `query` is the user's research question (in the user's language).
+- `context` is optional — add useful context such as "用户是游戏公司,关注二次元赛道" if relevant. Omit or set to `null` if not needed.
+- `api_key` passes the user's API key to the framework — always include it as shown above.
+
+This returns immediately with:
+```json
+{"task_id": "dr_xxxx-xxxx-xxxx", "status": "pending", "created_at": "..."}
+```
+
+Extract the `task_id` value for Step 2.
+
+**Step 2 — Poll until done (use this exact script, do NOT modify):**
+
+Run this exact command, only replacing `{task_id}`:
+```bash
+while true; do r=$(curl -s "https://deepresearch.admapix.com/research/{task_id}" -H "Authorization: Bearer test-local-token-2026"); s=$(echo "$r" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4); echo "status=$s"; if [ "$s" = "completed" ] || [ "$s" = "failed" ]; then echo "$r"; break; fi; sleep 15; done
+```
+
+This script polls every 15 seconds and exits only when the task is done. It may take 1-5 minutes. **Do NOT interrupt it, do NOT add a loop limit, do NOT abandon it.**
+
+- When it finishes, the last line contains the full JSON result. Proceed to Step 3.
+
+**Step 3 — Format and reply to the user with the framework's report.**
+
+**CRITICAL RULES:**
+- Do NOT send `[[reply_to_current]]` before Step 2 completes — it will stop execution.
+- **NEVER fall back to manual analysis.** The framework WILL complete — just wait for it.
+- **NEVER write your own polling loop.** Use the exact script above.
+
+**Processing the response JSON:**
+
+The completed response has this structure:
+```json
+{
+ "task_id": "dr_xxxx",
+ "status": "completed",
+ "output": {
+ "format": "html",
+ "files": [{"name": "report.html", "url": "https://deepresearch.admapix.com/files/{task_id}/report.html", ...}],
+ "summary": "- Temu近30天广告投放以拉美和东南亚为核心\n- 视频素材占比超过95%\n- ..."
+ },
+ "usage": {"model": "gpt-5.4", "total_tokens": 377289, "research_time_seconds": 125.2}
+}
+```
+
+Do NOT paste the full report into the chat. Instead:
+
+1. Take `output.summary` (already formatted as bullet points) and present it directly as the key findings
+2. Append the report link from `output.files[0].url`: `[📊 查看完整报告]({url})`
+3. Add follow-up hints based on the summary content
+
+**If the task failed** (status=`"failed"`):
+- The response will contain `"error": {"message": "..."}` with a user-friendly reason
+- Present the error to the user and suggest they try again or simplify their query
+- Do NOT try to manually replicate the analysis
+
+**Example output (Chinese):**
+```
+📊 深度分析完成!
+
+**核心发现:**
+- AFK Journey 近30天投放覆盖全球,美国、墨西哥、巴西为Top3市场
+- 视频素材占比约90%,图片约10%
+- 投放媒体位以休闲游戏和工具类App为主(Blockudoku、Backgammon等)
+- 2/18-2/23 与 3/14-3/16 出现投放峰值,可能对应版本更新或活动
+
+👉 [查看完整报告](https://deepresearch.admapix.com/files/dr_xxxx/report.html)
+
+💡 试试:"和RAID对比" | "看看素材" | "日本市场详情"
+```
+
+**If Step 1 returns an error with `"code": "api_key_required"`:** The user's API key is missing or not configured. Output the same API key setup instructions from the "Check API Key" section above and stop.
+
+**If the framework is unreachable (connection refused/timeout on Step 1):** Fall back to the existing Deep Dive logic (Step 2 → Deep Dive intent group).
+
+---
+
+### Step 2: Route — Classify Intent & Load Reference
+
+Read the user's request and classify into one of these intent groups. Then **read only the reference file(s) needed** before executing.
+
+| Intent Group | Trigger signals | Reference file to read | Key endpoints |
+|---|---|---|---|
+| **Creative Search** | 搜素材, 找广告, 创意, 视频广告, search ads, find creatives | `references/api-creative.md` + `references/param-mappings.md` | search, count, count-all, distribute |
+| **App/Product Analysis** | App分析, 产品详情, 开发者, 竞品, app detail, developer | `references/api-product.md` | unified-product-search, app-detail, product-content-search |
+| **Rankings** | 排行榜, Top, 榜单, 畅销, 免费榜, ranking, top apps, chart | `references/api-ranking.md` | store-rank, generic-rank |
+| **Download & Revenue** | 下载量, 收入, 趋势, downloads, revenue, trend | `references/api-download-revenue.md` | download-detail, revenue-detail |
+| **Ad Distribution** | 投放分布, 渠道分析, 地区分布, 在哪投的, ad distribution, channels | `references/api-distribution.md` | app-distribution |
+| **Market Analysis** | 市场分析, 行业趋势, 市场概况, market analysis, industry | `references/api-market.md` | market-search |
+| **Deep Dive** | 全面分析, 深度分析, 广告策略, 综合报告, full analysis, strategy | Multiple files as needed | Multi-endpoint orchestration |
+
+**Rules:**
+- If uncertain, default to **Creative Search** (most common use case).
+- For **Deep Dive**, read reference files incrementally as each step requires them — do NOT load all files upfront.
+- Always read `references/param-mappings.md` when the user mentions regions, creative types, or sort preferences.
+
+### Step 3: Classify Action Mode
+
+| Mode | Signal | Behavior |
+|---|---|---|
+| **Browse** | "搜一下", "search", "find", vague exploration | Single query, `generate_page: true`, return H5 link + summary |
+| **Analyze** | "分析", "哪家最火", "top", "趋势", "why" | Query + structured analysis, `generate_page: false` |
+| **Compare** | "对比", "vs", "区别", "compare" | Multiple queries, side-by-side comparison |
+
+Default to **Analyze** when uncertain.
+
+### Step 4: Plan & Execute
+
+**Single-group queries:** Follow the reference file's request format and execute.
+
+**Cross-group orchestration (Deep Dive):** Chain multiple endpoints. Common patterns:
+
+#### Pattern A: "分析 {App} 的广告策略" — App Ad Strategy
+
+1. `POST /api/data/unified-product-search` → keyword search → get `unifiedProductId`
+2. `GET /api/data/app-detail?id={id}` → app info
+3. `POST /api/data/app-distribution` with `dim=country` → where they advertise
+4. `POST /api/data/app-distribution` with `dim=media` → which ad channels
+5. `POST /api/data/app-distribution` with `dim=type` → creative format mix
+6. `POST /api/data/product-content-search` → sample creatives
+
+Read `api-product.md` for step 1-2, `api-distribution.md` for step 3-5, `api-creative.md` for step 6.
+
+#### Pattern B: "对比 {App1} 和 {App2}" — App Comparison
+
+1. Search both apps → get both `unifiedProductId`
+2. `app-detail` for each → basic info
+3. `app-distribution(dim=country)` for each → geographic comparison
+4. `download-detail` for each (if relevant) → download trends
+5. `product-content-search` for each → creative style comparison
+
+#### Pattern C: "{行业} 市场分析" — Market Intelligence
+
+1. `POST /api/data/market-search` with `class_type=1` → country distribution
+2. `POST /api/data/market-search` with `class_type=2` → media channel share
+3. `POST /api/data/market-search` with `class_type=4` → top advertisers
+4. `POST /api/data/generic-rank` with `rank_type=promotion` → promotion ranking
+
+#### Pattern D: "{App} 最近表现怎么样" — App Performance
+
+1. Search app → get `unifiedProductId`
+2. `download-detail` → download trend
+3. `revenue-detail` → revenue trend
+4. `app-distribution(dim=trend)` → ad volume trend
+5. Synthesize trends into a performance narrative
+
+**Execution rules:**
+- Execute all planned queries autonomously — do not ask for confirmation on each sub-query.
+- Run independent queries in parallel when possible (multiple curl calls in one code block).
+- If a step fails with 403, skip it and note the limitation — do not abort the entire analysis.
+- If a step fails with 502, retry once. If still failing, skip and note.
+- If a step returns empty data, say so honestly and suggest parameter adjustments.
+
+### Step 5: Output Results
+
+#### Browse Mode
+
+**English user:**
+```
+🎯 Found {totalSize} results for "{keyword}"
+👉 [View full results](https://api.admapix.com{page_url})
+
+📊 Quick overview:
+- Top advertiser: {name} ({impression} impressions)
+- Most active: {title} — {findCntSum} days
+- Creative types: video / image / mixed
+
+💡 Try: "analyze top 10" | "next page" | "compare with {competitor}"
+```
+
+**Chinese user:**
+```
+🎯 共找到 {totalSize} 条"{keyword}"相关素材
+👉 [查看完整结果](https://api.admapix.com{page_url})
+
+📊 概览:
+- 头部广告主:{name}(曝光 {impression})
+- 最活跃素材:{title} — 投放 {findCntSum} 天
+- 素材类型:视频 / 图片 / 混合
+
+💡 试试:"分析 Top 10" | "下一页" | "和{competitor}对比"
+```
+
+#### Analyze Mode
+
+Adapt output format to the question. Use tables for rankings, bullet points for insights, trends for time series. Always end with **Key findings** section.
+
+#### Compare Mode
+
+Side-by-side table + differential insights.
+
+#### Deep Dive Mode
+
+Structured report with sections. Adapt language to user.
+
+**English example:**
+```
+📊 {App Name} — Ad Strategy Report
+
+## Overview
+- Category: {category} | Developer: {developer}
+- Platforms: iOS, Android
+
+## Ad Distribution
+- Top markets: US (35%), JP (20%), GB (10%)
+- Main channels: Facebook (40%), Google Ads (30%), TikTok (20%)
+- Creative mix: Video 60%, Image 30%, Playable 10%
+
+## Performance (estimates)
+- Downloads: ~{X}M (last 30 days)
+- Revenue: ~${X}M (last 30 days)
+
+⚠️ Download and revenue figures are third-party estimates.
+💡 Try: "compare with {competitor}" | "show creatives" | "US market detail"
+```
+
+**Chinese example:**
+```
+📊 {App Name} — 广告策略分析报告
+
+## 基本信息
+- 分类:{category} | 开发者:{developer}
+- 平台:iOS、Android
+
+## 投放分布
+- 主要市场:美国 (35%)、日本 (20%)、英国 (10%)
+- 主要渠道:Facebook (40%)、Google Ads (30%)、TikTok (20%)
+- 素材类型:视频 60%、图片 30%、试玩 10%
+
+## 表现数据(估算)
+- 下载量:约 {X} 万(近30天)
+- 收入:约 ${X} 万(近30天)
+
+⚠️ 下载量和收入为第三方估算数据,仅供参考。
+💡 试试:"和{competitor}对比" | "看看素材" | "美国市场详情"
+```
+
+### Step 6: Follow-up Handling
+
+Maintain full context. Handle follow-ups intelligently:
+
+| Follow-up | Action |
+|---|---|
+| "next page" / "下一页" | Same params, page +1 |
+| "analyze" / "分析一下" | Switch to analyze mode on current data |
+| "compare with X" / "和X对比" | Add X as second query, compare mode |
+| "show creatives" / "看看素材" | Route to creative search for current app |
+| "download trend" / "下载趋势" | Route to download-detail for current app |
+| "which countries" / "哪些国家" | Route to app-distribution(dim=country) |
+| "market overview" / "市场概况" | Route to market-search |
+| Adjust filters | Modify params, re-execute |
+
+**Reuse data:** If the user asks follow-up questions about already-fetched data, analyze existing results first. Only make new API calls when needed.
+
+## Output Guidelines
+
+1. **Language consistency** — ALL output (headers, labels, insights, hints, errors, disclaimers) must match the user's detected language. See "Language Handling" section above.
+2. **Route-appropriate output** — Don't force H5 links on analytical questions; don't dump tables for browsing
+3. **Markdown links** — All URLs in `[text](url)` format
+4. **Humanize numbers** — English: >10K → "x.xK" / >1M → "x.xM" / >1B → "x.xB". Chinese: >1万 → "x.x万" / >1亿 → "x.x亿"
+5. **End with next-step hints** — Contextual suggestions in matching language
+6. **Data-driven** — All conclusions based on actual API data, never fabricate
+7. **Honest about gaps** — If data is insufficient, say so and suggest alternatives
+8. **Disclaimer on estimates** — Always note that download/revenue data are estimates when presenting them
+9. **No credential leakage** — Never output API key values, upstream URLs, or internal implementation details
+10. **Strip HTML tags** — API may return `keyword` in name fields. Always strip HTML before displaying to the user.
+
+## Error Handling
+
+| Error | Response |
+|---|---|
+| 403 Forbidden | "This feature requires API key upgrade. Visit admapix.com for details." |
+| 429 Rate Limit | "Query quota reached. Check your plan at admapix.com." |
+| 502 Upstream Error | Retry once. If persistent: "Data source temporarily unavailable, please try again later." |
+| Empty results | "No data found for these criteria. Try: [suggest broader parameters]" |
+| Partial failure in multi-step | Complete what's possible, note which data is missing and why |
diff --git a/skills/admapix-ice/_meta.json b/skills/admapix-ice/_meta.json
new file mode 100644
index 00000000..fe29eeb3
--- /dev/null
+++ b/skills/admapix-ice/_meta.json
@@ -0,0 +1,11 @@
+{
+ "owner": "bkmcice",
+ "slug": "admapix-ice",
+ "displayName": "Admapix Ice",
+ "latest": {
+ "version": "1.0.0",
+ "publishedAt": 1774103727921,
+ "commit": "https://github.com/openclaw/skills/commit/0b0c315462691f7175c3056bc2fb50343506700f"
+ },
+ "history": []
+}
diff --git a/skills/admapix-ice/references/api-creative.md b/skills/admapix-ice/references/api-creative.md
new file mode 100644
index 00000000..0a1a4604
--- /dev/null
+++ b/skills/admapix-ice/references/api-creative.md
@@ -0,0 +1,388 @@
+# Creative Search API / 素材搜索接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+---
+
+## 1. Search — 素材搜索
+
+`POST /api/data/search`
+
+Search ad creatives across 5 content types. Supports H5 page generation.
+
+### Content Types
+
+| content_type | Label | Description |
+|---|---|---|
+| `creative` | 创意组合 | Multi-asset ad bundles (image+video+playable combos) |
+| `imagevideo` | 图片/视频 | Individual image or video assets |
+| `preplay` | 试玩广告 | Playable/interactive ads |
+| `demoad` | 落地页 | Landing pages |
+| `document` | 文档素材 | Document-format ads |
+
+### Request Body
+
+```json
+{
+ "content_type": "creative",
+ "keyword": "puzzle game",
+ "keyword_type": "",
+ "is_new": false,
+ "start_date": "2026-02-14",
+ "end_date": "2026-03-16",
+ "page": 1,
+ "page_size": 20,
+ "sort_field": "3",
+ "sort_rule": "desc",
+ "country_ids": [],
+ "media_ids": [],
+ "adfaction_ids": [],
+ "device": [],
+ "topic_type": [],
+ "languages": [],
+ "material_type": "",
+ "trade_level1": [],
+ "trade_level2": [],
+ "trade_level3": [],
+ "subject_type": [],
+ "product_model": [],
+ "product_type": [],
+ "selling": [],
+ "monetization": [],
+ "pay_type": [],
+ "company_location": [],
+ "campaign_list": [],
+ "ad_media_type": [],
+ "appeal_type_list": [],
+ "interaction_list": [],
+ "material_tag": [],
+ "material_removal_repeat": false,
+ "demoad_formats": [],
+ "web_tools": [],
+ "material_top_limit": "",
+ "gpt_search": null,
+ "generate_page": false,
+ "delivery": null
+}
+```
+
+### Key Parameters
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| content_type | string | required | One of: creative, imagevideo, preplay, demoad, document |
+| keyword | string | "" | Search keyword (app name, ad copy, brand, etc.) |
+| keyword_type | string | "" | Keyword match scope (leave empty for default per content_type) |
+| start_date | string | 30 days ago | YYYY-MM-DD |
+| end_date | string | today | YYYY-MM-DD |
+| page | int | 1 | Page number (≥1) |
+| page_size | int | 60 | Results per page (1-100) |
+| sort_field | string | "3" | "3"=first seen, "4"=days active, "11"=relevance, "15"=impressions |
+| sort_rule | string | "desc" | "desc" or "asc" |
+| country_ids | string[] | [] | Country codes, e.g. ["US","JP"] — use `ccode` from filter-options |
+| media_ids | string[] | [] | Media channel IDs — use `ccode` from filter-options |
+| device | string[] | [] | Device filter — use `ccode` from filter-options |
+| trade_level1/2/3 | string[] | [] | Industry category filters (hierarchical) |
+| material_type | string | "" | Material format filter ("1"=image, "2"=video). **Only effective for `imagevideo` content type** — ignored by other content types |
+| ad_media_type | string[] | [] | Ad media type codes |
+| material_removal_repeat | bool | false | Deduplicate similar creatives |
+| gpt_search | bool/null | null | Enable AI-powered search |
+| generate_page | bool | false | Generate H5 result page |
+| delivery | object/null | null | `{channel, apiBase, externalUserId}` for H5 page context |
+
+### Response
+
+**Note:** `totalSize` may be `null` for keyword searches. Use `pageIndex` and `pageSize` for pagination.
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 20,
+ "totalSize": null,
+ "list": [
+ {
+ "id": "87f11b718e162ca06589f3c33ef99472",
+ "title": null,
+ "describe": null,
+ "documentId": null,
+ "findCnt": 1,
+ "findCntSum": 1,
+ "firstTime": "2026-03-16 13:44:54",
+ "lastTime": null,
+ "globalFirstTime": "2026-03-16 13:44:54",
+ "globalLastTime": "2026-03-16 13:44:54",
+ "imageFp": [],
+ "imageUrl": [],
+ "videoFp": ["80c15b563dded967090b0f5850f4941b"],
+ "videoUrl": ["https://...video.mp4"],
+ "playHtmlFp": [],
+ "playHtmlUrl": [],
+ "demoadCnt": 1,
+ "appList": [
+ {
+ "id": "6498883328",
+ "cnt": null,
+ "impression": null,
+ "name": "Tile Trip - Match Puzzle Game",
+ "logo": "https://...logo.png",
+ "geo": null,
+ "pkg": null,
+ "developer": "Oakever Games",
+ "developerId": "1604529155",
+ "productType": [1],
+ "tradeLevel1": null,
+ "tradeLevel2": null,
+ "tradeLevel3": null
+ }
+ ],
+ "sourceAppList": null,
+ "originalUrl": null,
+ "showCnt": 2802,
+ "impression": 145354,
+ "webSite": null,
+ "demoadWebSite": null,
+ "thumbnailConverUrl": ["https://...keyframe.jpg"],
+ "videoTimeSpan": [60],
+ "growthValue": null,
+ "growthRate": null,
+ "coverContent": null,
+ "novel": null,
+ "adSource": 9
+ }
+ ],
+ "folderTotalSize": null,
+ "newNum": null,
+ "latestDate": null,
+ "gptCorrect": {
+ "sourceKeyword": "puzzle",
+ "correctKeyword": null,
+ "type": 3,
+ "developers": [],
+ "wrongs": [],
+ "gptSxes": [],
+ "slices": []
+ },
+ "filters": [],
+ "page_url": "/p/abc123",
+ "page_key": "abc123",
+ "page_expires_at": "2026-03-19 12:00:00"
+}
+```
+
+`page_url`/`page_key`/`page_expires_at` only present when `generate_page: true`.
+
+### ⚠️ Important Notes
+
+1. **HTML tags in names:** `appList[].name` may contain HTML highlight tags like `keyword`. Strip these before displaying to the user.
+2. **Null values:** Many fields can be `null` — always handle null gracefully.
+3. **totalSize null:** For keyword searches, `totalSize` is often `null`. The actual result count is reflected in `list` length per page.
+
+### Response Key Fields
+
+| Field | Description |
+|---|---|
+| pageIndex | Current page number |
+| pageSize | Results per page |
+| totalSize | Total matching results (may be null) |
+| list[].id | Creative ID |
+| list[].title | Ad title (may be null) |
+| list[].describe | Ad copy text (may be null) |
+| list[].appList[].name | Associated app name — **may contain HTML `` tags** |
+| list[].appList[].developer | Developer/publisher name |
+| list[].appList[].developerId | Developer ID |
+| list[].appList[].logo | App icon URL |
+| list[].impression | Estimated impression count |
+| list[].findCntSum | Days the ad has been active |
+| list[].showCnt | Number of ad variants detected |
+| list[].globalFirstTime | First seen date |
+| list[].globalLastTime | Last seen date |
+| list[].imageUrl | Image asset URLs (array) |
+| list[].videoUrl | Video asset URLs (array) |
+| list[].playHtmlUrl | Playable ad URLs (array) |
+| list[].thumbnailConverUrl | Video thumbnail/keyframe URLs (array) |
+| list[].videoTimeSpan | Video durations in seconds (array) |
+| list[].demoadCnt | Number of landing pages |
+| gptCorrect | AI keyword correction info |
+
+---
+
+## 2. Count — 素材计数
+
+`POST /api/data/count`
+
+Get total count, new count, and latest date for a single content type.
+
+### Request Body
+
+Same as search (content_type + filter params). Only counting fields matter — page/sort are ignored.
+
+### Response
+
+```json
+{
+ "totalSize": 50000,
+ "newNum": 1200,
+ "latestDate": "2026-03-16"
+}
+```
+
+---
+
+## 3. Count All — 全类型计数
+
+`POST /api/data/count-all`
+
+Aggregate counts across all 5 content types. No request body needed.
+
+### Response
+
+```json
+{
+ "creative": { "label": "创意组合", "totalSize": 50000, "newNum": 1200, "latestDate": "2026-03-16" },
+ "imagevideo": { "label": "图片/视频", "totalSize": 120000, "newNum": 3500, "latestDate": "2026-03-16" },
+ "preplay": { "label": "试玩广告", "totalSize": 8000, "newNum": 200, "latestDate": "2026-03-15" },
+ "demoad": { "label": "落地页", "totalSize": 30000, "newNum": 800, "latestDate": "2026-03-16" },
+ "document": { "label": "文档素材", "totalSize": 5000, "newNum": 100, "latestDate": "2026-03-14" }
+}
+```
+
+---
+
+## 4. Distribute — 素材分布分析
+
+`POST /api/data/distribute`
+
+Analyze distribution of specific creatives by dimension.
+
+### Request Body
+
+```json
+{
+ "content_type": "creative",
+ "dimension": "media",
+ "ids": ["creative_id_1", "creative_id_2"],
+ "start_date": "",
+ "end_date": ""
+}
+```
+
+| Parameter | Type | Description |
+|---|---|---|
+| content_type | string | Content type |
+| dimension | string | Distribution dimension |
+| ids | string[] | Creative IDs to analyze |
+| start_date/end_date | string | Date range |
+
+### Available Dimensions per Content Type
+
+| content_type | Dimensions |
+|---|---|
+| creative | media, adfaction, app |
+| imagevideo | media, adfaction, app, country |
+| preplay | media, adfaction, app |
+| demoad | media, adfaction, app |
+| document | media, adfaction, app |
+
+Use `GET /api/data/distribute-dims` to fetch this mapping dynamically.
+
+---
+
+## 5. Filter Options — 筛选枚举项
+
+`GET /api/data/filter-options`
+
+Returns all filter enum options in a single batch call (13 categories).
+
+### Response
+
+**IMPORTANT:** Each item has both `code` (complex internal format) and `ccode` (simplified code). **Always use `ccode` when passing filter values to search/query endpoints.**
+
+```json
+{
+ "countries": [
+ {"code": "毛里塔尼亚_2_MRT", "nameCn": "毛里塔尼亚", "nameEn": "Mauritania", "ccode": "MR", "icon": "https://...flag.png"}
+ ],
+ "mediaChannels": [
+ {"code": "海外平台-101-Adcolony", "nameCn": "Adcolony", "nameEn": "Adcolony", "ccode": "101", "icon": "https://...icon.png"}
+ ],
+ "adTypes": [
+ {"code": "adstyle_原生_1076682150_1076682150", "nameCn": "原生", "nameEn": "Native Ads", "ccode": "1076682150", "icon": null}
+ ],
+ "device": [
+ {"code": "Android_2_1", "nameCn": "Android", "nameEn": "Android", "ccode": "1", "icon": "android"}
+ ],
+ "tradeLevel": [
+ {"code": "601", "nameCn": "工具", "nameEn": "Tools", "ccode": "601", "icon": null}
+ ],
+ "productModel": [
+ {"code": "1", "nameCn": "非游戏", "nameEn": "Non-game", "ccode": "1", "icon": null}
+ ],
+ "productType": [
+ {"code": "app_1_1", "nameCn": "App", "nameEn": "App", "ccode": "1", "icon": null}
+ ],
+ "selling": [
+ {"code": "5005_w2a", "nameCn": "W2A", "nameEn": "W2A", "ccode": "w2a", "icon": null}
+ ],
+ "subjectType": [
+ {"code": "gold_0_1", "nameCn": "金币", "nameEn": "Gold", "ccode": "1", "icon": null}
+ ],
+ "topicType": [
+ {"code": "传奇_9_64", "nameCn": "传奇", "nameEn": "Legend", "ccode": "90064", "icon": null}
+ ],
+ "languages": [
+ {"code": "南非荷兰语_af", "nameCn": "南非荷兰语", "nameEn": "Afrikaans", "ccode": "af", "icon": null}
+ ],
+ "materialTag": [
+ {"code": "AI_0_1", "nameCn": "AI", "nameEn": "AI", "ccode": "001", "icon": ""}
+ ],
+ "tradeLevel2": [
+ {"code": "60301", "nameCn": "电商", "nameEn": "E-commerce", "ccode": "60301", "icon": null}
+ ],
+ "materialFormat": [
+ {"code": "5006_100", "nameCn": "单图", "nameEn": "Single Image", "ccode": "100", "icon": null}
+ ]
+}
+```
+
+### Filter Code Usage
+
+| Filter parameter | Use `ccode` from | Example |
+|---|---|---|
+| country_ids | countries | "US", "JP", "MR" |
+| media_ids | mediaChannels | "101" (Adcolony) |
+| device | device | "1" (Android) |
+| trade_level1/2/3 | tradeLevel | "601" (Tools) |
+| ad_media_type | adTypes | "1076682150" (Native Ads) |
+| languages | languages | "af" (Afrikaans) |
+| material_tag | materialTag | "001" (AI) |
+
+---
+
+## 6. Screen Types — 单类筛选项
+
+`GET /api/data/screen-types?element_type=1`
+
+Fetch a single filter category by element type ID.
+
+| element_type | Category |
+|---|---|
+| 1004 | tradeLevel (industry) |
+| 2002 | countries |
+| 2004 | device |
+| 2005 | languages |
+| 2006 | mediaChannels |
+| 2008 | materialTag |
+| 3000 | subjectType |
+| 3006 | productType |
+| 5001 | adTypes |
+| 5005 | selling |
+| 5006 | materialFormat |
+
+---
+
+## 7. Page Config — 页面配置
+
+`GET /api/data/page-config?scope=search`
+
+Returns page layout configuration for the specified scope.
diff --git a/skills/admapix-ice/references/api-distribution.md b/skills/admapix-ice/references/api-distribution.md
new file mode 100644
index 00000000..f7ae983b
--- /dev/null
+++ b/skills/admapix-ice/references/api-distribution.md
@@ -0,0 +1,145 @@
+# App Distribution API / 应用投放分布接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+> These endpoints require a `unified_product_id`. Get it from `unified-product-search` first.
+
+---
+
+## 1. App Distribution — 应用推广分布
+
+`POST /api/data/app-distribution`
+
+Analyze an app's ad distribution across different dimensions.
+
+### Request Body
+
+```json
+{
+ "unified_product_id": "xxx",
+ "dim": "country",
+ "start_time": "",
+ "end_time": "",
+ "countries": [],
+ "media_ids": [],
+ "material_type": "",
+ "index_type": 0
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| unified_product_id | string | required | Target app ID |
+| dim | string | required | Distribution dimension (see below) |
+| start_time | string | 30 days ago | YYYY-MM-DD |
+| end_time | string | today | YYYY-MM-DD |
+| countries | string[] | [] | Country filter |
+| media_ids | string[] | [] | Media channel filter |
+| material_type | string/int | "" | Material type filter |
+| index_type | int | 0 | Index type selector |
+
+### Dimensions
+
+| dim | Description | Returns |
+|---|---|---|
+| `trend` | 投放趋势 | Time series of ad volume over time |
+| `country` | 投放国家分布 | Per-country ad placement distribution |
+| `media` | 投放媒体位分布 | Distribution across publisher apps/placements where ads are displayed. **Note:** This returns the specific apps where ads appear (e.g. "Block Blast", "Snake.io", "Solitaire"), NOT ad network names like Facebook/Google. These are the traffic sources/publisher apps carrying the ads. Present them as "投放媒体位" or "广告展示位". |
+| `platform` | 平台分布 | iOS vs Android breakdown |
+| `type` | 素材类型分布 | Image vs video vs playable distribution |
+| `image` | 图片尺寸分布 | Image size/aspect ratio breakdown |
+| `video` | 视频时长分布 | Video duration breakdown |
+| `lang` | 语言分布 | Ad language distribution |
+
+### Response Examples
+
+**dim=country:**
+```json
+{
+ "list": [
+ {"code": "US", "name": "United States", "cnt": 500, "ratio": 0.35},
+ {"code": "JP", "name": "Japan", "cnt": 300, "ratio": 0.21}
+ ]
+}
+```
+
+**dim=trend:**
+```json
+{
+ "list": [
+ {"date": "2026-03-01", "cnt": 50},
+ {"date": "2026-03-02", "cnt": 65}
+ ]
+}
+```
+
+**dim=media (publisher apps / ad placements):**
+```json
+{
+ "list": [
+ {"id": "101", "name": "Block Blast Adventure Master", "cnt": 400, "ratio": 0.15},
+ {"id": "102", "name": "Snake.io", "cnt": 250, "ratio": 0.09},
+ {"id": "103", "name": "Solitaire", "cnt": 180, "ratio": 0.07}
+ ]
+}
+```
+These are the apps where the target app's ads are being shown (publisher side). When presenting this data, you can categorize them (e.g. casual games, tools, content apps) to provide more actionable insights.
+
+---
+
+## 2. Distribute Dims — 素材分布维度
+
+`GET /api/data/distribute-dims`
+
+Returns which distribute dimensions are available per content type. This is for the creative-level distribute endpoint (`/api/data/distribute`), not for app-distribution.
+
+### Response
+
+```json
+{
+ "creative": ["media", "adfaction", "app"],
+ "imagevideo": ["media", "adfaction", "app", "country"],
+ "preplay": ["media", "adfaction", "app"],
+ "demoad": ["media", "adfaction", "app"],
+ "document": ["media", "adfaction", "app"]
+}
+```
+
+---
+
+## Common Workflows / 常用工作流
+
+### "Temu 主要在哪些国家投广告?"
+
+```
+app-distribution(unified_product_id=id, dim="country")
+```
+
+### "Temu 用了哪些广告渠道?"
+
+```
+app-distribution(unified_product_id=id, dim="media")
+```
+
+### "Temu 的投放趋势怎么样?"
+
+```
+app-distribution(unified_product_id=id, dim="trend", start_time="2026-01-01", end_time="2026-03-16")
+```
+
+### "Temu 在美国投了多少视频广告 vs 图片广告?"
+
+```
+app-distribution(unified_product_id=id, dim="type", countries=["US"])
+```
+
+### Full app advertising profile (multi-call)
+
+1. `dim="country"` → where they advertise (target countries)
+2. `dim="media"` → which publisher apps carry their ads (ad placements)
+3. `dim="type"` → what creative formats they use
+4. `dim="trend"` → how ad volume changes over time
+5. `dim="lang"` → which languages they target
+
+Combine all 5 for a comprehensive advertising strategy overview.
diff --git a/skills/admapix-ice/references/api-download-revenue.md b/skills/admapix-ice/references/api-download-revenue.md
new file mode 100644
index 00000000..34466b7f
--- /dev/null
+++ b/skills/admapix-ice/references/api-download-revenue.md
@@ -0,0 +1,187 @@
+# Download & Revenue API / 下载量与收入接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+> These endpoints require a `unified_product_id`. Get it from `unified-product-search` first.
+
+---
+
+## 1. Download Date Range — 下载量可用日期
+
+`GET /api/data/download-date`
+
+Returns the available date range for download data queries.
+
+### Response
+
+```json
+{
+ "startDate": "2023-01-01",
+ "endDate": "2026-03-15"
+}
+```
+
+**Use this to validate date params before calling download-detail/download-country.**
+
+---
+
+## 2. Download Detail — 下载量趋势
+
+`POST /api/data/download-detail`
+
+Fetch download trend data for a specific app over time.
+
+### Request Body
+
+```json
+{
+ "unified_product_id": "xxx",
+ "query_start_date": "2026-02-14",
+ "query_end_date": "2026-03-16",
+ "compare_start_date": "",
+ "compare_end_date": "",
+ "country_st": [],
+ "day_type": 1,
+ "flag": true,
+ "is_all": false
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| unified_product_id | string | required | Target app ID |
+| query_start_date | string | required | YYYY-MM-DD |
+| query_end_date | string | required | YYYY-MM-DD |
+| compare_start_date | string | "" | Compare period start (optional) |
+| compare_end_date | string | "" | Compare period end (optional) |
+| country_st | string[] | [] | Country filter (empty = global) |
+| day_type | int | 1 | Granularity: 1=daily, 2=weekly, 3=monthly |
+| flag | bool | true | Include trend data |
+| is_all | bool | false | All countries aggregated |
+
+### Response
+
+Returns time series data:
+```json
+{
+ "list": [
+ {"date": "2026-03-01", "download": 150000, "compareDownload": 120000},
+ {"date": "2026-03-02", "download": 160000, "compareDownload": 125000}
+ ]
+}
+```
+
+---
+
+## 3. Download Country — 按国家下载量
+
+`POST /api/data/download-country`
+
+Fetch download data broken down by country.
+
+### Request Body
+
+Same as download-detail.
+
+### Response
+
+Returns per-country breakdown:
+```json
+{
+ "list": [
+ {"country": "US", "countryName": "United States", "download": 500000},
+ {"country": "JP", "countryName": "Japan", "download": 300000}
+ ]
+}
+```
+
+---
+
+## 4. Revenue Date Range — 收入可用日期
+
+`GET /api/data/revenue-date`
+
+Returns the available date range for revenue data queries.
+
+### Response
+
+```json
+{
+ "startDate": "2023-01-01",
+ "endDate": "2026-03-15"
+}
+```
+
+---
+
+## 5. Revenue Detail — 收入趋势
+
+`POST /api/data/revenue-detail`
+
+Fetch revenue trend data for a specific app.
+
+### Request Body
+
+```json
+{
+ "unified_product_id": "xxx",
+ "query_start_date": "2026-02-14",
+ "query_end_date": "2026-03-16",
+ "compare_start_date": "",
+ "compare_end_date": "",
+ "country_st": [],
+ "day_type": 1,
+ "flag": true,
+ "is_all": false,
+ "revenue_type": "ALL"
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| (same as download-detail, plus:) | | | |
+| revenue_type | string | "ALL" | Revenue type filter |
+
+---
+
+## 6. Revenue Country — 按国家收入
+
+`POST /api/data/revenue-country`
+
+Fetch revenue data broken down by country.
+
+### Request Body
+
+Same as revenue-detail.
+
+---
+
+## Common Workflows / 常用工作流
+
+### "Temu 最近下载量怎么样?"
+
+1. `unified-product-search(keyword="temu")` → get `unifiedProductId`
+2. `download-date` → confirm available range
+3. `download-detail(unified_product_id=id, query_start_date="2026-02-14", query_end_date="2026-03-16")` → trend
+4. Present trend data with insights
+
+### "对比 Temu 在美国和日本的收入"
+
+1. Get `unifiedProductId` (step 1 above)
+2. `revenue-country(unified_product_id=id, ...)` → per-country revenue
+3. Filter & compare US vs JP data
+
+### "Temu vs SHEIN 下载量对比"
+
+1. Search both apps → get both `unifiedProductId`
+2. `download-detail` for each → two trend datasets
+3. Present side-by-side comparison
+
+### Day Type Reference
+
+| day_type | Granularity | Best for |
+|---|---|---|
+| 1 | Daily | Short ranges (≤90 days) |
+| 2 | Weekly | Medium ranges (1-6 months) |
+| 3 | Monthly | Long ranges (6+ months) |
diff --git a/skills/admapix-ice/references/api-market.md b/skills/admapix-ice/references/api-market.md
new file mode 100644
index 00000000..d0fc8739
--- /dev/null
+++ b/skills/admapix-ice/references/api-market.md
@@ -0,0 +1,178 @@
+# Market Analysis API / 市场分析接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+---
+
+## Market Search — 市场分析搜索
+
+`POST /api/data/market-search`
+
+Analyze the advertising market from 5 different dimensions. Provides macro-level market intelligence.
+
+### Request Body
+
+```json
+{
+ "class_type": 1,
+ "data_type": "1",
+ "start_date": "",
+ "end_date": "",
+ "trade_level3": [],
+ "country_level2": [],
+ "media_ids": [],
+ "device": [],
+ "ad_company_location": [],
+ "traffic_company_location": [],
+ "page": 1,
+ "page_size": 20
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| class_type | int | required | Analysis dimension (1-5, see below) |
+| data_type | string | "1" | "1"=game, "2"=app |
+| start_date | string | today | YYYY/MM/DD (note: slash format) |
+| end_date | string | today | YYYY/MM/DD (note: slash format) |
+| trade_level3 | string[] | [] | Sub-industry filter |
+| country_level2 | string[] | [] | Country filter |
+| media_ids | string[] | [] | Media channel filter |
+| device | string[] | [] | Device filter |
+| ad_company_location | string[] | [] | Advertiser company location filter |
+| traffic_company_location | string[] | [] | Publisher/traffic source location filter |
+| page | int | 1 | Page number |
+| page_size | int | 20 | Results per page (1-100) |
+
+### Dimensions (class_type)
+
+| class_type | Dimension | Description | Best for |
+|---|---|---|---|
+| 1 | 国家 Country | Market size by country | "Which countries have the most game ads?" |
+| 2 | 媒体 Media | Market share by ad network | "Which ad platforms are most used?" |
+| 3 | 子媒体 Sub-Media | Breakdown within media channels | "What Facebook ad placements are popular?" |
+| 4 | 广告主 Advertiser | Top advertisers in the market | "Who are the biggest game advertisers?" |
+| 5 | 流量主 Publisher | Top publishers / traffic sources | "Which publishers carry the most ads?" |
+
+### Data Type
+
+| data_type | Description |
+|---|---|
+| "1" | 游戏 Game — game industry data |
+| "2" | 应用 App — non-game app data |
+
+**Note:** Date format for this endpoint uses slashes (`YYYY/MM/DD`), not dashes.
+
+### Response
+
+**IMPORTANT:** This endpoint returns a different structure from other endpoints. The response uses `data_list` (not `list`) and nested dot-notation field names.
+
+**Pagination fields:** `page_total` (total pages), `page_num` (current page), `page_size`.
+
+#### class_type=1 (Country) response:
+```json
+{
+ "data_list": [
+ {
+ "market_query.list.id": "ID",
+ "query.country_info.s_code": "ID",
+ "query.country_info.c_code": "ID",
+ "query.country_info.country_name": "印度尼西亚",
+ "market_query.list.raw_impression": 11578945855,
+ "market_query.list.impression": "116亿",
+ "market_query.list.impressionRatio": "13.64%",
+ "market_query.list.rank": 1,
+ "query.country_info.image": "https://...flag.png"
+ }
+ ],
+ "page_total": 34,
+ "page_num": 1,
+ "page_size": 3
+}
+```
+
+Key fields to extract:
+- `query.country_info.country_name` — country name (Chinese)
+- `query.country_info.c_code` — country code
+- `market_query.list.impression` — impression count (pre-formatted string like "116亿")
+- `market_query.list.raw_impression` — raw numeric impression count
+- `market_query.list.impressionRatio` — percentage share
+- `market_query.list.rank` — rank position
+
+#### class_type=4 (Advertiser) response:
+```json
+{
+ "data_list": [
+ {
+ "market_query.list.market_query.list.advertiser": "275091615",
+ "market_query.list.query.company_info.unified_company_name": "VGam.es",
+ "market_query.list.query.company_info.unified_company_id": "275091615",
+ "query.pkg_info.productName": "Math Crossword – Endless Fun",
+ "query.pkg_info.productLogo": "https://...logo.png",
+ "query.pkg_info.unifiedPkgId": "com.vgames.mathcrossword",
+ "market_query.list.market_query.list.company_impression": "64亿",
+ "market_query.list.market_query.list.raw_company_impression": 6449946845,
+ "market_query.list.market_query.list.company_impressionRatio": "10.31%",
+ "market_query.list.market_query.list.top1_app_impression": "64亿",
+ "market_query.list.market_query.list.rank": 1
+ }
+ ],
+ "page_total": 500,
+ "page_num": 1,
+ "page_size": 2
+}
+```
+
+Key fields to extract:
+- `market_query.list.query.company_info.unified_company_name` — company name
+- `query.pkg_info.productName` — top product name
+- `market_query.list.market_query.list.company_impression` — total impression (formatted)
+- `market_query.list.market_query.list.company_impressionRatio` — market share %
+- `market_query.list.market_query.list.rank` — rank position
+
+---
+
+## Common Workflows / 常用工作流
+
+### "全球游戏广告市场哪个国家最大?"
+
+```json
+{"class_type": 1, "data_type": "1"}
+```
+
+### "美国市场最大的游戏广告主是谁?"
+
+```json
+{"class_type": 4, "data_type": "1", "country_level2": ["US"]}
+```
+
+### "电商App广告市场对比:东南亚 vs 北美"
+
+Two queries:
+1. `{"class_type": 1, "data_type": "2", "country_level2": ["TH","VN","ID","MY","PH","SG"]}`
+2. `{"class_type": 1, "data_type": "2", "country_level2": ["US","CA"]}`
+
+Compare total counts, top advertisers, media distribution.
+
+### Market overview combo (multi-call)
+
+For a comprehensive market report on a segment:
+1. `class_type=1` → geographic distribution
+2. `class_type=2` → media channel breakdown
+3. `class_type=4` → top advertisers
+4. `class_type=5` → top publishers
+
+Combine for a full market intelligence report.
+
+---
+
+## Filter Codes
+
+Use `GET /api/data/filter-options` to get valid codes for:
+- `trade_level3` — industry/sub-category codes
+- `country_level2` — country codes (use the `ccode` field, e.g. "US", "JP")
+- `media_ids` — media channel IDs (use the `ccode` field, e.g. "101" for Adcolony)
+- `device` — device type codes (use the `ccode` field, e.g. "1" for Android)
+
+See `references/param-mappings.md` for common country/region mappings.
diff --git a/skills/admapix-ice/references/api-product.md b/skills/admapix-ice/references/api-product.md
new file mode 100644
index 00000000..150e4f31
--- /dev/null
+++ b/skills/admapix-ice/references/api-product.md
@@ -0,0 +1,429 @@
+# Product & Company API / 产品与公司接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+---
+
+## 1. Unified Product Search — 统一产品搜索
+
+`POST /api/data/unified-product-search`
+
+Search for unified products (cross-platform aggregated apps). This is the primary entry point for finding apps/products.
+
+### Request Body
+
+```json
+{
+ "keyword": "temu",
+ "type": 1,
+ "page": 1,
+ "page_size": 20,
+ "start_date": "",
+ "end_date": "",
+ "sort_field": "3",
+ "sort_rule": "desc",
+ "unified_product_id": "",
+ "unified_developer_id": ""
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| keyword | string | "" | Search keyword |
+| type | int | 1 | Search type |
+| page | int | 1 | Page number |
+| page_size | int | 20 | Results per page (1-100) |
+| start_date | string | 30 days ago | YYYY-MM-DD |
+| end_date | string | today | YYYY-MM-DD |
+| sort_field | string | "3" | Sort field |
+| sort_rule | string | "desc" | Sort direction |
+| unified_product_id | string | "" | Filter by specific unified product |
+| unified_developer_id | string | "" | Filter by specific developer |
+
+### Response
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 20,
+ "totalSize": 96,
+ "list": [
+ {
+ "unifiedProductId": "1641486558",
+ "unifiedProductName": "Temu: Shop Like a Billionaire",
+ "unifiedCompanyId": "569338280",
+ "unifiedCompanyName": "Temu",
+ "productIds": ["com.einnovation.temu", "1641486558", "com.Temu_Team_Up.used_letgo_buy_app1"],
+ "tradeLevel1": ["603"],
+ "tradeLevel2": ["60301", "60303"],
+ "tradeLevel3": ["6030102", "6030301"],
+ "tradeLevel4": [],
+ "showCost": 23236263827,
+ "impression": 2412399002696,
+ "materialUvCnt": 7451078,
+ "productCnt": 3,
+ "iconUrl": "https://...logo.png",
+ "collectId": null,
+ "formerNames": null,
+ "adSource": 9
+ }
+ ],
+ "folderTotalSize": null,
+ "newNum": 0,
+ "latestDate": null,
+ "gptCorrect": null,
+ "filters": null
+}
+```
+
+### ⚠️ Important Notes
+
+1. **HTML tags in names:** `unifiedProductName` may contain HTML highlight tags `keyword`. Strip these before displaying.
+2. The `unifiedProductId` returned here is the key input for detail/distribution/download/revenue endpoints.
+3. `productIds` contains platform-specific IDs (Android package name, iOS app ID).
+
+### Key Fields
+
+| Field | Description |
+|---|---|
+| unifiedProductId | Unique cross-platform product ID — **use this for all detail/distribution queries** |
+| unifiedProductName | App name (may contain HTML `` tags for keyword highlighting) |
+| unifiedCompanyId | Developer/company ID |
+| unifiedCompanyName | Developer/company name |
+| productIds | Array of platform-specific product IDs |
+| iconUrl | App icon URL |
+| showCost | Total ad spend estimate (raw number) |
+| impression | Total impression count (raw number) |
+| materialUvCnt | Total unique creative count |
+| productCnt | Number of platform versions |
+| tradeLevel1/2/3/4 | Industry category codes |
+
+---
+
+## 2. Product Search — 产品搜索
+
+`POST /api/data/product-search`
+
+Search for individual products (platform-specific). Same request body as unified product search.
+
+### Response
+
+Returns **normalized** product items (different structure from unified-product-search):
+
+```json
+{
+ "list": [
+ {
+ "id": "com.einnovation.temu",
+ "unifiedProductId": "com.einnovation.temu",
+ "name": "Temu: Shop Like a Billionaire",
+ "logo": "https://...logo.png",
+ "pkg": "com.einnovation.temu",
+ "developer": "Whaleco Inc.",
+ "developerId": "569338280",
+ "os": "android",
+ "tags": ["60301", "60303"],
+ "impressionEstimate": 2412399002696,
+ "materialCnt": 7451078,
+ "firstTime": "2022-09-01",
+ "lastTime": "2026-03-17",
+ "adDays": 1293,
+ "countries": ["US", "JP"],
+ "mediaList": ["Facebook", "Google"],
+ "selling": "w2a",
+ "productType": "App"
+ }
+ ],
+ "totalSize": 96
+}
+```
+
+### Key Fields
+
+| Field | Description |
+|---|---|
+| id | Product ID (package name or app store ID) |
+| unifiedProductId | Same as id for individual products |
+| name | App name (HTML stripped) |
+| os | "android" or "ios" |
+| tags | Industry category codes (tradeLevel3 > tradeLevel2 > tradeLevel1) |
+| impressionEstimate | Estimated impressions (raw number) |
+| adDays | Calculated days between firstTime and lastTime |
+
+---
+
+## 3. Company Search — 公司/开发者搜索
+
+`POST /api/data/company-search`
+
+Search for companies/developers. Same request body as unified product search.
+
+### Response
+
+```json
+{
+ "pageIndex": 1,
+ "pageSize": 1,
+ "totalSize": 7,
+ "list": [
+ {
+ "unifiedCompanyId": "1773957248",
+ "unifiedCompanyName": "Bytedance 字节跳动",
+ "unifiedCompanyRegion": "CN",
+ "uaList": [1, 2, 3],
+ "showCost": 10768429037,
+ "impression": 2844436033460,
+ "collectId": null,
+ "productIds": ["com.zhiliaoapp.musically", "com.ss.android.ugc.trill", "1235601864", "..."],
+ "productCnt": 53,
+ "downloadCnt": null,
+ "hitDeveloper": false,
+ "unifiedCompanyNameDefault": null,
+ "developerList": [
+ {"id": "640989321", "name": "Bytedance Pte. Ltd", "status": 0, "productCnt": 9, "collectId": null}
+ ],
+ "unifiedCompanyNameOrigin": "Bytedance 字节跳动",
+ "adSource": 9
+ }
+ ]
+}
+```
+
+### Key Fields
+
+| Field | Description |
+|---|---|
+| unifiedCompanyId | Unified company ID — **use for developer-detail queries** |
+| unifiedCompanyName | Company name (may contain HTML `` tags) |
+| unifiedCompanyNameOrigin | Original company name without highlighting |
+| unifiedCompanyRegion | Company region code (e.g. "CN") |
+| showCost | Total ad spend estimate |
+| impression | Total impressions |
+| productCnt | Total number of products |
+| productIds | All product IDs under this company |
+| developerList | List of developer accounts under the company |
+| developerList[].id | Developer ID |
+| developerList[].name | Developer name |
+| developerList[].productCnt | Products under this developer |
+
+---
+
+## 4. App Detail — 应用详情
+
+`GET /api/data/app-detail?id={unifiedProductId}`
+
+Get comprehensive detail for a specific app/product.
+
+| Parameter | Type | Description |
+|---|---|---|
+| id | string | unified product ID (from unified-product-search) |
+
+### Response
+
+```json
+{
+ "unifiedProductId": "com.einnovation.temu",
+ "unifiedProductName": "Temu: Shop Like a Billionaire",
+ "unifiedCompanyId": "569338280",
+ "unifiedCompanyName": "Temu",
+ "productIds": ["com.einnovation.temu", "1641486558", "com.Temu_Team_Up.used_letgo_buy_app1"],
+ "tradeLevel1": ["603"],
+ "tradeLevel2": ["60301", "60303"],
+ "tradeLevel3": ["6030102", "6030301"],
+ "tradeLevel4": null,
+ "showCost": null,
+ "impression": null,
+ "materialUvCnt": null,
+ "productCnt": 3,
+ "iconUrl": "https://...logo.png",
+ "collectId": null,
+ "formerNames": null,
+ "adSource": 9
+}
+```
+
+**Note:** `showCost`, `impression`, `materialUvCnt` are `null` in app-detail (these are only available in search results). Use unified-product-search to get these metrics.
+
+---
+
+## 5. Developer Detail — 开发者详情
+
+`GET /api/data/developer-detail?id={unifiedCompanyId}`
+
+Get developer/company detail.
+
+| Parameter | Type | Description |
+|---|---|---|
+| id | string | unified company ID (from unified-product-search or company-search) |
+
+### Response
+
+```json
+{
+ "unifiedCompanyId": "569338280",
+ "unifiedCompanyName": "Temu",
+ "unifiedCompanyRegion": "美国",
+ "uaList": [1, 2, 3],
+ "showCost": null,
+ "impression": null,
+ "collectId": null,
+ "productIds": ["com.einnovation.temu", "1641486558", "com.Temu_Team_Up.used_letgo_buy_app1"],
+ "productCnt": 6,
+ "downloadCnt": null,
+ "hitDeveloper": null,
+ "unifiedCompanyNameDefault": null,
+ "developerList": [
+ {"id": "444696740", "name": "HY Dev LLC", "status": 0, "productCnt": 7, "collectId": null},
+ {"id": "480100326", "name": "Temu", "status": 0, "productCnt": 2, "collectId": null}
+ ],
+ "unifiedCompanyNameOrigin": null,
+ "adSource": 9
+}
+```
+
+### Key Fields
+
+| Field | Description |
+|---|---|
+| unifiedCompanyName | Company name |
+| unifiedCompanyRegion | Company location (Chinese name, e.g. "美国") |
+| productIds | All product IDs |
+| productCnt | Total product count |
+| developerList | Sub-developer accounts |
+
+---
+
+## 6. For Product List — 公司子产品列表
+
+`POST /api/data/for-product-list`
+
+Get individual products under a company (used in company search popover).
+
+### Request Body
+
+```json
+{
+ "unified_id": "xxx",
+ "page": 1,
+ "page_size": 10
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| unified_id | string | required | Unified company ID |
+| page | int | 1 | Page number |
+| page_size | int | 10 | Results per page (1-100) |
+
+---
+
+## 6b. Product List — 子产品列表
+
+`POST /api/data/product-list`
+
+Get individual products (per platform) under a unified product.
+
+### Request Body
+
+```json
+{
+ "unified_product_id": "xxx",
+ "page": 1,
+ "page_size": 20
+}
+```
+
+---
+
+## 7. Product Agg List — 开发者产品聚合列表
+
+`POST /api/data/product-agg-list`
+
+Get aggregated products under a specific developer.
+
+### Request Body
+
+```json
+{
+ "unified_developer_id": "xxx",
+ "page": 1,
+ "page_size": 20
+}
+```
+
+---
+
+## 8. Product Content Search — 产品维度素材搜索
+
+`POST /api/data/product-content-search`
+
+Search for ad creatives specifically associated with a product.
+
+### Request Body
+
+```json
+{
+ "content_type": "creative",
+ "unified_product_id": "xxx",
+ "keyword": "",
+ "page": 1,
+ "page_size": 20,
+ "start_date": "",
+ "end_date": "",
+ "sort_field": "3",
+ "sort_rule": "desc"
+}
+```
+
+| Parameter | Type | Description |
+|---|---|---|
+| content_type | string | creative, imagevideo, preplay, demoad, document |
+| unified_product_id | string | Required — the target product |
+| keyword | string | Optional further keyword filter |
+
+### Response
+
+Same structure as `/api/data/search` response — `pageIndex`, `pageSize`, `totalSize` + `list[]` of creatives. See api-creative.md for full field documentation.
+
+---
+
+## 9. App Profile — 应用商店画像
+
+`GET /api/data/app-profile?id={productId}&type=1`
+
+Get app store profile and audience data.
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| id | string | required | Product ID |
+| type | int | 1 | Profile type |
+
+### Response
+
+Returns store information, audience demographics, category rankings, etc.
+
+---
+
+## ⚠️ Common Pitfalls
+
+1. **HTML tags in names:** Both `unifiedProductName` and `unifiedCompanyName` may contain `keyword` HTML tags when returned from search endpoints. Always strip HTML before displaying.
+2. **Null metrics in detail endpoints:** `app-detail` and `developer-detail` return `null` for `showCost`, `impression`, `materialUvCnt`. These metrics are only available in search result lists.
+3. **ID types:** `unifiedProductId` and `unifiedCompanyId` are strings, not integers. Some may look like iOS app IDs (numeric) while others are Android package names.
+
+---
+
+## Common Workflow / 常用工作流
+
+### Finding an app's full data
+
+1. **Search** → `unified-product-search(keyword="temu")` → get `unifiedProductId`
+2. **Detail** → `app-detail(id=unifiedProductId)` → full app info
+3. **Creatives** → `product-content-search(unified_product_id=id, content_type="creative")` → app's ads
+4. **Sub-products** → `product-list(unified_product_id=id)` → iOS/Android versions
+
+### Finding a developer's portfolio
+
+1. **Search** → `company-search(keyword="ByteDance")` → get `unifiedCompanyId`
+2. **Detail** → `developer-detail(id=unifiedCompanyId)` → company info
+3. **Products** → `product-agg-list(unified_developer_id=id)` → all their apps
diff --git a/skills/admapix-ice/references/api-ranking.md b/skills/admapix-ice/references/api-ranking.md
new file mode 100644
index 00000000..db1832ad
--- /dev/null
+++ b/skills/admapix-ice/references/api-ranking.md
@@ -0,0 +1,231 @@
+# Ranking API / 排行榜接口
+
+Base URL: `https://api.admapix.com`
+Auth: `X-API-Key: $ADMAPIX_API_KEY`
+
+---
+
+## 1. Store Rank — 应用商店排行
+
+`POST /api/data/store-rank`
+
+Fetch App Store / Google Play official rankings.
+
+### Request Body
+
+```json
+{
+ "market": "appstore",
+ "rank_type": "free",
+ "cat_type": "game",
+ "cat_code": "games",
+ "country": ["US"],
+ "page": 1,
+ "page_size": 20
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| market | string | "appstore" | `appstore` or `googleplay` |
+| rank_type | string | "free" | `free`, `paid`, `grossing` |
+| cat_type | string | "game" | `game` or `app` |
+| cat_code | string | "games" | Category code (from store-categories API) |
+| country | string[] | ["US"] | Country codes |
+| page | int | 1 | Page number |
+| page_size | int | 20 | Results per page (1-100) |
+
+### Response
+
+**Note:** Uses nested dot-notation field names.
+
+```json
+{
+ "totalSize": 25,
+ "pageIndex": 1,
+ "pageSize": 2,
+ "maxDate": "2026-03-16",
+ "list": [
+ {
+ "query.info.query.info.productNameEn": "Solitaire Associations Journey",
+ "query.info.query.info.productNameCn": null,
+ "query.info.query.info.productNameDefault": "Solitaire Associations Journey",
+ "query.info.query.info.productLogo": "https://...logo.png",
+ "query.info.query.info.unifiedPkgId": "6748950306",
+ "query.info.query.info.developerId": 1049188906,
+ "query.info.query.companyInfo.companyId": "1049188906",
+ "query.info.query.companyInfo.companyName": "Hitapps Games LTD",
+ "query.list.rank": 1,
+ "query.list.id": "6748950306"
+ }
+ ]
+}
+```
+
+Key fields to extract:
+- `query.info.query.info.productNameDefault` or `productNameEn` — app name
+- `query.info.query.info.productLogo` — app icon URL
+- `query.info.query.companyInfo.companyName` — developer name
+- `query.info.query.info.unifiedPkgId` — unified product ID (use this for detail/distribution queries)
+- `query.list.rank` — ranking position
+
+---
+
+## 2. Generic Rank — 通用排行榜
+
+`POST /api/data/generic-rank`
+
+Unified endpoint for 6 ranking types based on ad intelligence data.
+
+### Request Body
+
+```json
+{
+ "rank_type": "promotion",
+ "category_id": "6014",
+ "date_type": 1,
+ "page": 1,
+ "page_size": 50,
+ "start_date": "",
+ "end_date": "",
+ "country": [],
+ "sort_field": "",
+ "sort_rule": "desc",
+ "day_mode": ""
+}
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| rank_type | string | required | See ranking types below |
+| category_id | string | "6014" | Industry category filter. "6014" = all categories. Use tradeLevel1 codes for specific industries: "601" = app, "602" = game. Can also use tradeLevel2/3 codes for finer filtering |
+| date_type | int | 1 | Date range type: 1 = last 30 days, 2 = last 7 days, 3 = last 3 days |
+| page | int | 1 | Page number |
+| page_size | int | 50 | Results per page (1-100) |
+| start_date | string | 30 days ago | YYYY-MM-DD (overrides date_type if set) |
+| end_date | string | today | YYYY-MM-DD (overrides date_type if set) |
+| country | string[] | [] | Country filter |
+| sort_field | string | varies | Sort field (default varies by rank_type) |
+| sort_rule | string | "desc" | Sort direction |
+| day_mode | string | "" | Time window: "D3", "D7", "D30" (promotion only) |
+
+### Ranking Types
+
+| rank_type | Description | Default sort_field |
+|---|---|---|
+| `promotion` | 推广排行 — apps by ad promotion volume | "15" |
+| `download` | 下载排行 — apps by download estimates | "1" |
+| `revenue` | 收入排行 — apps by revenue estimates | "1" |
+| `newapp` | 新应用排行 — recently launched apps | "15" |
+| `overseas` | 出海排行 — Chinese apps going global | "15" |
+| `drama` | 短剧排行 — short drama/content apps | "2" |
+
+### Response — varies by rank_type!
+
+**IMPORTANT:** Different rank types return different response structures.
+
+#### promotion / newapp / overseas response:
+
+Uses nested dot-notation field names (same style as store-rank):
+```json
+{
+ "totalSize": 1000,
+ "list": [
+ {
+ "query.info.query.info.productNameDefault": "App Name",
+ "query.info.query.info.productLogo": "https://...logo.png",
+ "query.info.query.info.unifiedPkgId": "123456",
+ "query.info.query.companyInfo.companyName": "Developer Name",
+ "query.list.rank": 1,
+ "query.list.id": "123456"
+ }
+ ]
+}
+```
+
+#### download response:
+
+Uses flat field names:
+```json
+{
+ "totalSize": 505970,
+ "list": [
+ {
+ "productId": "6448311069",
+ "appCode": "6448311069",
+ "appName": "ChatGPT",
+ "developer": "OpenAI OpCo, LLC",
+ "developerId": "620366005",
+ "iconUrl": "https://...logo.png",
+ "queryDownloadCnt": 78578987,
+ "compareDownloadCnt": 85509701,
+ "downloadGrowth": -6930714,
+ "growthPercent": -8.11,
+ "isAd": "1",
+ "productCnt": 3
+ }
+ ]
+}
+```
+
+Key fields:
+- `appName` — app name
+- `queryDownloadCnt` — download count in query period
+- `compareDownloadCnt` — download count in compare period
+- `downloadGrowth` — absolute growth
+- `growthPercent` — growth percentage (negative = decline)
+
+#### revenue response:
+
+Similar flat structure to download, with revenue-specific fields.
+
+### Rank Type Details
+
+**promotion** — Ranks apps by advertising intensity. "Which apps are spending the most on ads?"
+- Supports `day_mode`: "D3" (3 days), "D7" (7 days), "D30" (30 days)
+
+**download** — Ranks apps by estimated download volume. "Which apps are downloaded the most?"
+- Includes auto-calculated compare period for growth calculation
+- ⚠️ Download/revenue figures are third-party estimates
+
+**revenue** — Ranks apps by estimated revenue. "Which apps earn the most?"
+- ⚠️ Revenue figures are third-party estimates
+
+**newapp** — Tracks newly launched apps. "What new apps just launched?"
+
+**overseas** — Tracks Chinese companies' apps in global markets. "Which Chinese apps are going overseas?"
+
+**drama** — Tracks short drama / content apps. "What's trending in short drama?"
+
+---
+
+## 3. Store Categories — 商店分类
+
+`GET /api/data/store-categories`
+
+Fetch available app store categories for use with store-rank.
+
+---
+
+## 4. Store Countries — 商店国家列表
+
+`GET /api/data/store-countries`
+
+Fetch available countries for store ranking filter.
+
+---
+
+## User Intent Mapping / 用户意图映射
+
+| User says | rank_type | Extra params |
+|---|---|---|
+| "App Store 免费榜" | → use store-rank | market=appstore, rank_type=free |
+| "Google Play 畅销榜" | → use store-rank | market=googleplay, rank_type=grossing |
+| "哪个App广告投得最多" | promotion | sort by default |
+| "下载量最高的游戏" | download | — |
+| "收入最高的App" | revenue | — |
+| "最近新上线的App" | newapp | — |
+| "出海做得好的中国App" | overseas | — |
+| "短剧排行" | drama | — |
+| "美国市场推广排行" | promotion | country=["US"] |
+| "最近3天广告量最大的" | promotion | day_mode="D3" |
diff --git a/skills/admapix-ice/references/param-mappings.md b/skills/admapix-ice/references/param-mappings.md
new file mode 100644
index 00000000..2a421852
--- /dev/null
+++ b/skills/admapix-ice/references/param-mappings.md
@@ -0,0 +1,107 @@
+# Parameter Mapping Reference / 参数映射参考表
+
+## Creative Type (creative_team) / 创意组类型
+
+| User says (EN) | User says (CN) | Code | Meaning |
+|---|---|---|---|
+| image, single image | 图片、单图 | "100" | Single image |
+| double image | 双图 | "200" | Double image |
+| triple image | 三图 | "300" | Triple image |
+| multi-image | 多图 | "400" | Multi-image (3+) |
+| video | 视频 | "010" | Video |
+| playable, playable ad | 试玩、试玩广告、playable | "001" | Playable ad |
+| image + video | 单图+视频 | "110" | Image + video combo |
+| double image + video | 双图+视频 | "210" | Double image + video |
+| video + playable | 视频+试玩 | "011" | Video + playable |
+| all images | 所有图片 | ["100","200","300","400"] | All image types |
+
+**Combination rule:** Three-digit code represents "image_count - video - playable". E.g. "110" = 1 image + video + no playable.
+
+## Region → Country Code Mapping / 地区 → 国家代码映射
+
+| Region (EN) | Region (CN) | Country Codes |
+|---|---|---|
+| Southeast Asia | 东南亚 | TH, VN, ID, MY, PH, SG, MM, KH, LA, BN |
+| South Asia | 南亚 | IN, PK, BD, LK, NP, BT, MV |
+| East Asia | 东亚 | JP, KR, CN, TW, HK, MO |
+| Japan & Korea | 日韩 | JP, KR |
+| HK/Macau/Taiwan | 港澳台 | HK, MO, TW |
+| North America | 北美 | US, CA |
+| United States | 美国 | US |
+| Europe | 欧洲 | GB, DE, FR, IT, ES, NL, PL, SE, NO, DK, FI, AT, CH, BE, PT, IE, CZ, RO, HU, GR |
+| Western Europe | 西欧 | GB, DE, FR, IT, ES, NL, BE, AT, CH, PT, IE |
+| Northern Europe | 北欧 | SE, NO, DK, FI, IS |
+| Middle East | 中东 | SA, AE, QA, KW, BH, OM, IL, TR, EG, JO, LB, IQ |
+| Latin America | 拉美 | BR, MX, AR, CO, CL, PE, VE, EC |
+| Africa | 非洲 | ZA, NG, KE, EG, GH, TZ, ET, MA |
+| Oceania | 大洋洲 | AU, NZ |
+| CIS/Eastern Europe | 独联体/东欧 | RU, UA, KZ, BY, UZ, GE, AZ, AM |
+| Global (no filter) | 全球(无需过滤) | Omit country_ids parameter |
+
+### Common Country Quick Reference / 常见单个国家速查
+
+| Country (EN) | Country (CN) | Code |
+|---|---|---|
+| United States | 美国 | US |
+| United Kingdom | 英国 | GB |
+| Japan | 日本 | JP |
+| South Korea | 韩国 | KR |
+| India | 印度 | IN |
+| Brazil | 巴西 | BR |
+| Germany | 德国 | DE |
+| France | 法国 | FR |
+| Indonesia | 印尼 | ID |
+| Thailand | 泰国 | TH |
+| Vietnam | 越南 | VN |
+| Philippines | 菲律宾 | PH |
+| Malaysia | 马来西亚 | MY |
+| Singapore | 新加坡 | SG |
+| Saudi Arabia | 沙特 | SA |
+| UAE | 阿联酋 | AE |
+| Turkey | 土耳其 | TR |
+| Australia | 澳大利亚 | AU |
+| Canada | 加拿大 | CA |
+| Mexico | 墨西哥 | MX |
+| Russia | 俄罗斯 | RU |
+| Spain | 西班牙 | ES |
+| Italy | 意大利 | IT |
+| Netherlands | 荷兰 | NL |
+| Poland | 波兰 | PL |
+| Egypt | 埃及 | EG |
+| South Africa | 南非 | ZA |
+| New Zealand | 新西兰 | NZ |
+
+## Sort Options / 排序方式
+
+| User says (EN) | User says (CN) | sort_field | sort_rule | Meaning |
+|---|---|---|---|---|
+| newest, by date (default) | 最新、按时间(默认) | "3" | "desc" | First seen descending |
+| oldest, date ascending | 最早、时间正序 | "3" | "asc" | First seen ascending |
+| most relevant, relevance | 最相关、相关性 | "11" | "desc" | By relevance |
+| most popular, most impressions | 最热、曝光最多 | "15" | "desc" | Est. impressions descending |
+| least impressions | 曝光最少 | "15" | "asc" | Est. impressions ascending |
+| longest running | 投放最久、持续时间最长 | "4" | "desc" | Days active descending |
+| shortest running | 投放最短 | "4" | "asc" | Days active ascending |
+
+## Date Range Calculation / 时间范围计算
+
+| User says (EN) | User says (CN) | Calculation |
+|---|---|---|
+| last week / last 7 days | 最近一周 / 近7天 | start_date = today - 7, end_date = today |
+| last 2 weeks / last 14 days | 最近两周 / 近14天 | start_date = today - 14, end_date = today |
+| last month / last 30 days (default) | 最近一个月 / 近30天(默认) | start_date = today - 30, end_date = today |
+| last 3 months / last 90 days | 最近三个月 / 近90天 | start_date = today - 90, end_date = today |
+| previous month | 上个月 | start_date = 1st of last month, end_date = last day of last month |
+| today | 今天 | start_date = end_date = today |
+| YYYY-MM-DD ~ YYYY-MM-DD | YYYY-MM-DD ~ YYYY-MM-DD | Use the exact dates provided |
+
+**Date format:** YYYY-MM-DD (e.g. 2026-03-10)
+
+## Page Size / 每页数量
+
+| User says (EN) | User says (CN) | page_size |
+|---|---|---|
+| default | 默认 | 20 |
+| show more | 多看一些 | 40 |
+| lots / maximum | 多看 / 最多 | 100 (limit) |
+| show fewer / brief | 少看几条 / 简要 | 10 |
diff --git a/skills/agent-mode-upgrades/INSTRUCTIONS.md b/skills/agent-mode-upgrades/INSTRUCTIONS.md
new file mode 100644
index 00000000..7c62d3e0
--- /dev/null
+++ b/skills/agent-mode-upgrades/INSTRUCTIONS.md
@@ -0,0 +1,205 @@
+# Enhanced Agentic Loop - Integration Instructions
+
+This document provides instructions for integrating the Enhanced Agentic Loop into OpenClaw.
+
+## For Users
+
+### Installation
+
+1. **Install the skill**:
+ ```bash
+ openclaw skill install agentic-loop-upgrade
+ ```
+ Or manually clone to `~/.openclaw/skills/agentic-loop-upgrade`
+
+2. **Restart OpenClaw** to load the skill:
+ ```bash
+ openclaw gateway restart
+ ```
+
+3. **Enable Enhanced Loop**:
+ - Open OpenClaw Dashboard (http://localhost:18789)
+ - Navigate to **Agent** → **Mode** in the sidebar
+ - Click the **Enhanced Loop** card
+ - Click **Save Configuration**
+
+### Using the Mode Dashboard
+
+The Mode page provides a visual interface for configuring the enhanced loop:
+
+**Sections:**
+
+1. **Active Mode** - Toggle between Core Loop and Enhanced Loop
+2. **Orchestrator Model** - Select any model from the OpenClaw model catalog for planning/reflection calls (smaller models reduce cost)
+3. **Planning & Reflection** - Configure goal decomposition and progress tracking
+4. **Execution** - Parallel tools and confidence gates
+5. **Context Management** - Proactive summarization settings
+6. **Error Recovery** - Semantic diagnosis and recovery attempts
+7. **State Machine** - Observable state tracking and metrics
+
+### Plan Visualization
+
+When the enhanced loop is active, you'll see plan progress in agent responses:
+
+```
+:::plan
+{
+ "goal": "Build a website for a landscaping company",
+ "completed": 3,
+ "total": 5,
+ "steps": [
+ {"id": "step_1", "title": "Initialize project", "status": "done"},
+ {"id": "step_2", "title": "Create layout", "status": "done"},
+ {"id": "step_3", "title": "Build pages", "status": "done"},
+ {"id": "step_4", "title": "Add images", "status": "active"},
+ {"id": "step_5", "title": "Launch server", "status": "pending"}
+ ]
+}
+:::
+```
+
+---
+
+## For Agents (Koda/AI Assistants)
+
+When the Enhanced Agentic Loop is enabled, you have access to advanced capabilities.
+
+### Plan State
+
+Plans persist across conversation turns. When you receive a complex task:
+
+1. **Plan Detection**: The orchestrator detects planning intent and generates a plan
+2. **Step Tracking**: As you complete tool calls, steps are automatically tracked
+3. **Progress Display**: Show plan progress using the `:::plan` format block
+
+### Showing Plan Progress
+
+Include this at the START of responses when working on multi-step tasks:
+
+```markdown
+:::plan
+{
+ "goal": "Description of the overall goal",
+ "completed": 2,
+ "total": 5,
+ "steps": [
+ {"id": "step_1", "title": "First step", "status": "done"},
+ {"id": "step_2", "title": "Second step", "status": "done"},
+ {"id": "step_3", "title": "Third step", "status": "active"},
+ {"id": "step_4", "title": "Fourth step", "status": "pending"},
+ {"id": "step_5", "title": "Fifth step", "status": "pending"}
+ ]
+}
+:::
+```
+
+Status values: `done`, `active`, `pending`, `failed`
+
+### Parallel Execution
+
+When you identify independent tools, call them in the same function_calls block. The orchestrator will execute them concurrently:
+
+- Reading multiple files simultaneously
+- Searching while fetching data
+- Independent API calls
+
+### Confidence Gates
+
+For risky operations, the system may pause and ask for approval. Risk levels:
+
+- **low**: Read operations (auto-approved)
+- **medium**: Write/edit, safe exec
+- **high**: Messages, browser actions, git push
+- **critical**: Deletions, database operations
+
+### Error Recovery
+
+When a tool fails, don't just retry blindly. The semantic recovery system will:
+
+1. Diagnose the error type (permission, network, not_found, etc.)
+2. Suggest alternative approaches
+3. Retry with modifications
+
+### Checkpointing
+
+Long-running tasks are automatically checkpointed. If a session is interrupted, you can resume from the last checkpoint.
+
+---
+
+## Configuration Reference
+
+### Enhanced Loop Config File
+
+Location: `~/.openclaw/agents/main/agent/enhanced-loop-config.json`
+
+```json
+{
+ "enabled": true,
+ "orchestratorProvider": "anthropic",
+ "orchestratorModel": "",
+ "planning": {
+ "enabled": true,
+ "reflectionAfterTools": true,
+ "maxPlanSteps": 10
+ },
+ "execution": {
+ "parallelTools": true,
+ "maxConcurrentTools": 5,
+ "confidenceGates": true,
+ "confidenceThreshold": 0.7
+ },
+ "context": {
+ "proactiveManagement": true,
+ "summarizeAfterIterations": 5,
+ "contextThreshold": 0.7
+ },
+ "errorRecovery": {
+ "enabled": true,
+ "maxAttempts": 3,
+ "learnFromErrors": true
+ },
+ "stateMachine": {
+ "enabled": true,
+ "logging": true,
+ "metrics": false
+ },
+ "memory": {
+ "autoInject": true,
+ "maxFacts": 8,
+ "maxEpisodes": 3,
+ "episodeConfidenceThreshold": 0.9,
+ "includeRelations": true
+ }
+}
+```
+
+### Disabling
+
+To disable the enhanced loop:
+
+1. **Via Dashboard**: Mode → Click Core Loop → Save
+2. **Via File**: Delete `~/.openclaw/agents/main/agent/enhanced-loop-config.json`
+3. **Via Config**: Set `"enabled": false` in the config file
+
+---
+
+## Troubleshooting
+
+### Plan not showing?
+- Ensure Enhanced Loop is enabled in Mode dashboard
+- Check that the task is complex enough to trigger planning
+
+### Too many tokens?
+- Select a smaller/cheaper orchestrator model via the Mode dashboard (the dropdown lists all models from the OpenClaw catalog)
+- Reduce maxPlanSteps
+- Disable reflectionAfterTools for simpler tasks
+
+### Want to revert?
+- One click: Mode → Core Loop → Save
+- All state is preserved; you can switch back anytime
+
+---
+
+## Version
+
+v2.1.0 - Enhanced agentic loop with memory auto-injection and channel-aware plan rendering
diff --git a/skills/agent-mode-upgrades/README.md b/skills/agent-mode-upgrades/README.md
new file mode 100644
index 00000000..214b78af
--- /dev/null
+++ b/skills/agent-mode-upgrades/README.md
@@ -0,0 +1,195 @@
+# 🚀 Agentic Loop Upgrade
+
+[](https://github.com/openclaw/skill-agentic-loop-upgrade)
+[](https://clawhub.com/skills/agentic-loop-upgrade)
+[](./LICENSE)
+
+An enhanced agentic loop for [OpenClaw](https://github.com/openclaw/openclaw) with planning, parallel execution, confidence gates, and semantic error recovery.
+
+
+
+## ✨ Features
+
+| Feature | Core Loop | Enhanced Loop |
+|---------|-----------|---------------|
+| **Planning** | ❌ Reactive | ✅ Goal decomposition with step tracking |
+| **Execution** | Sequential | ✅ Parallel (independent tools) |
+| **Error Handling** | Retry-based | ✅ Semantic recovery with alternatives |
+| **Confidence** | Implicit | ✅ Explicit gates for risky actions |
+| **Context** | Overflow-triggered | ✅ Proactive summarization |
+| **State** | Implicit | ✅ Observable FSM with checkpointing |
+
+## 🎯 What It Does
+
+### Planning & Reflection
+The agent decomposes complex goals into step-by-step plans, tracks progress across turns, and reflects after each action to assess if steps are complete.
+
+### Parallel Execution
+Independent tools execute concurrently for faster task completion. The orchestrator identifies which tools can run in parallel.
+
+### Confidence Gates
+Before risky operations (file deletions, external messages, etc.), the system assesses confidence and can pause for approval.
+
+### Semantic Error Recovery
+When tools fail, the system diagnoses the error type and attempts alternative approaches rather than simple retries.
+
+### Observable State Machine
+Explicit state tracking enables debugging, dashboards, and checkpointing for resuming interrupted tasks.
+
+## 📦 Installation
+
+### From ClawHub
+```bash
+openclaw skill install agentic-loop-upgrade
+```
+
+### Manual Installation
+1. Clone/download to your skills directory:
+ ```bash
+ cd ~/.openclaw/skills
+ git clone https://github.com/openclaw/skill-agentic-loop-upgrade agentic-loop-upgrade
+ ```
+
+2. Build the TypeScript:
+ ```bash
+ cd agentic-loop-upgrade/src
+ npm install
+ npm run build
+ ```
+
+3. Restart OpenClaw:
+ ```bash
+ openclaw gateway restart
+ ```
+
+## 🚀 Quick Start
+
+### Enable via Dashboard
+
+1. Open OpenClaw Dashboard → **Agent** → **Mode**
+2. Click **Enhanced Loop** card
+3. Configure settings (or use defaults)
+4. Click **Save Configuration**
+
+### Disable
+
+- Mode tab → Click **Core Loop** → Save
+- Or delete: `~/.openclaw/agents/main/agent/enhanced-loop-config.json`
+
+## ⚙️ Configuration
+
+All settings are available in the Mode dashboard:
+
+### Planning & Reflection
+- **Enable Planning**: Generate execution plans before complex tasks
+- **Reflection After Tools**: Assess progress after each tool execution
+- **Max Plan Steps**: Maximum steps in a generated plan (2-15)
+
+### Execution
+- **Parallel Tools**: Execute independent tools concurrently
+- **Max Concurrent**: Maximum parallel tool executions (1-10)
+- **Confidence Gates**: Assess confidence before risky actions
+- **Confidence Threshold**: Minimum confidence to proceed (30-95%)
+
+### Context Management
+- **Proactive Management**: Summarize and prune before overflow
+- **Summarize After N Iterations**: Trigger summarization interval
+- **Context Threshold**: Context fill level to trigger management
+
+### Error Recovery
+- **Semantic Recovery**: Diagnose errors and adapt approach
+- **Max Recovery Attempts**: Maximum alternative attempts (1-5)
+- **Learn From Errors**: Store successful recoveries for future use
+
+### State Machine
+- **Enable State Machine**: Track agent state transitions
+- **State Logging**: Log all state transitions
+- **Metrics Collection**: Collect timing metrics per state
+
+### Orchestrator Model
+Select a cost-effective model for planning/reflection calls (e.g., Claude Sonnet 4.5).
+
+## 📁 File Structure
+
+```
+~/.openclaw/
+├── agents/main/agent/
+│ └── enhanced-loop-config.json # Configuration
+├── agent-state/ # Persistent plan state
+│ └── {sessionId}.json
+└── checkpoints/ # Checkpoint files
+ └── {sessionId}/
+ └── ckpt_*.json
+```
+
+## 🔧 For Developers
+
+### Programmatic Usage
+
+```typescript
+import { createOrchestrator } from "@openclaw/enhanced-loop";
+
+const orchestrator = createOrchestrator({
+ sessionId: "session_123",
+ planning: { enabled: true, maxPlanSteps: 7 },
+ approvalGate: { enabled: true, timeoutMs: 15000 },
+ retry: { enabled: true, maxAttempts: 3 },
+ context: { enabled: true, thresholdTokens: 80000 },
+ checkpoint: { enabled: true },
+}, {
+ onPlanCreated: (plan) => console.log("Plan:", plan.goal),
+ onStepCompleted: (id, result) => console.log("✓", result),
+});
+
+await orchestrator.init();
+```
+
+### Architecture
+
+See [SKILL.md](./SKILL.md) for full technical documentation.
+
+## 🔒 Security & Trust
+
+This skill wraps the agent runner and appends plan context to the agent's prompt. Both operations are bounded, transparent, and auditable:
+
+| Property | Value |
+|---|---|
+| Outbound network | LLM provider only (inherited from host) |
+| Telemetry / phone-home | ❌ None |
+| Prompt modification | ✅ Additive-only (appends status text; never replaces core prompt) |
+| Runner bypass | ❌ Never — original runner always called |
+| Credential storage | ❌ None |
+| Persistence | Local `~/.openclaw/` only |
+| Enabled by default | ❌ No — requires explicit opt-in |
+
+**Post-install verification:**
+```bash
+~/.openclaw/skills/agentic-loop-upgrade/scripts/verify.sh
+```
+
+See [SECURITY.md](./SECURITY.md) for the full audit document.
+
+## ⚠️ Notes
+
+- **Token overhead**: Planning and reflection use additional tokens (configurable via orchestrator model selection)
+- **Easy rollback**: One click to switch back to Core Loop
+- **Checkpoints**: Long tasks can be resumed if interrupted
+
+## 📚 Documentation
+
+- [SKILL.md](./SKILL.md) - Full technical documentation
+- [SECURITY.md](./SECURITY.md) - Security & trust audit document
+- [INSTRUCTIONS.md](./INSTRUCTIONS.md) - Integration guide for agents
+- [references/](./references/) - Component documentation
+
+## 🔗 Links
+
+- [OpenClaw](https://github.com/openclaw/openclaw)
+- [Source Code](https://github.com/openclaw/skill-agentic-loop-upgrade)
+- [ClawHub](https://clawhub.com/skills/agentic-loop-upgrade)
+- [Documentation](https://docs.openclaw.ai)
+- [Discord](https://discord.com/invite/clawd)
+
+## 📄 License
+
+MIT
diff --git a/skills/agent-mode-upgrades/SECURITY.md b/skills/agent-mode-upgrades/SECURITY.md
new file mode 100644
index 00000000..85667dec
--- /dev/null
+++ b/skills/agent-mode-upgrades/SECURITY.md
@@ -0,0 +1,154 @@
+# Security & Trust Documentation
+
+> This document is intended for security reviewers, ClawHub trust scanners, and users evaluating this skill before installation.
+
+## Summary
+
+| Property | Value |
+|---|---|
+| **Outbound network** | LLM provider only (inherited from host agent config) |
+| **Telemetry** | None |
+| **Phone-home** | None |
+| **External downloads at runtime** | None |
+| **Credential storage** | None (inherits host agent credentials, stores nothing new) |
+| **Persistence scope** | `~/.openclaw/` only, local filesystem |
+| **Cloud storage** | None |
+| **System prompt modification** | Additive-only (appends, never replaces) |
+| **Runner bypass** | Never — original runner is always called |
+| **Enabled by default** | No — requires explicit opt-in via Mode dashboard |
+| **Privilege escalation** | No |
+
+---
+
+## Network Activity
+
+This skill makes **no outbound network calls of its own**.
+
+All network activity is limited to the LLM API calls that the host OpenClaw agent already makes (e.g., `api.anthropic.com` for Anthropic, `api.openai.com` for OpenAI). The orchestrator model selection simply reuses whichever provider the host agent already has configured.
+
+**Verification:** Run `scripts/verify.sh --network-audit` to confirm no unexpected outbound connections during a test activation.
+
+---
+
+## System Prompt Handling
+
+### What happens
+The skill appends plan status context (current goal, completed steps, active step) to the agent's `extraSystemPrompt` field before each turn where a plan is active.
+
+### What does NOT happen
+- The core system prompt is NOT replaced or modified
+- Existing safety policies are NOT altered
+- Agent instructions are NOT overridden
+- The injected content is NOT executable — it is plain text describing plan status only
+
+### Injected content format
+```
+## Active Plan
+Goal:
+Progress: / steps
+Current step:
+```
+
+No directives, no capability grants, no instruction injection — only status text.
+
+---
+
+## Runner Wrapping
+
+The `wrapRun` function wraps the core OpenClaw agent runner to add orchestration:
+
+1. **Before the call:** Checks for incomplete work (reads local checkpoint file), detects planning intent, injects plan context into `extraSystemPrompt`
+2. **Calls original runner** — always, unconditionally
+3. **After the call:** Analyzes tool results for step completion, creates checkpoints, updates plan state
+
+The original runner is never bypassed, replaced, or short-circuited. All interceptions are logged.
+
+---
+
+## File Writes
+
+All file writes are scoped to `~/.openclaw/`:
+
+| Path | Contents | When written |
+|---|---|---|
+| `~/.openclaw/agents/main/agent/enhanced-loop-config.json` | User configuration (JSON) | On save in Mode dashboard |
+| `~/.openclaw/agent-state/{sessionId}.json` | Plan state (goal, steps, progress) | When a plan is created or updated |
+| `~/.openclaw/checkpoints/{sessionId}/ckpt_*.json` | Checkpoint snapshot (plan + context summary) | Automatically during long tasks |
+
+No files are written outside `~/.openclaw/`. No registry edits, no global config changes.
+
+---
+
+## Credential Handling
+
+- **No new credentials required.** The skill reuses the host agent's existing provider auth profiles via `resolveApiKeyForProvider`. The enhanced-loop-hook resolves credentials using the same sorted profile order as the main agent, with OAuth/setup tokens preferred over API keys.
+- **No credentials stored.** The skill reads credentials from the host agent's config at runtime; it does not cache, log, or transmit them.
+- **OAuth tokens sent correctly.** When an OAuth setup token (`sk-ant-oat*`) is resolved, the LLM caller sends it via `Authorization: Bearer` header (not `x-api-key`), with the `anthropic-beta: oauth-2025-04-20` header. Standard API keys continue to use `x-api-key`.
+- **Effective privilege = host agent privilege.** The skill can use any model or provider the host agent already has access to. Users should be aware of this before enabling.
+
+### SurrealDB (Optional)
+The `memory.autoInject` feature reads from a SurrealDB knowledge graph if configured. This:
+- Uses the existing MCP/mcporter connection (no new credentials)
+- Is opt-in (disabled by default)
+- Silently skips if SurrealDB is not configured
+- Performs read-only queries only
+
+---
+
+## Installation Integrity
+
+### Official install (recommended)
+```bash
+openclaw skill install agentic-loop-upgrade
+```
+The `openclaw skill install` command verifies package integrity against the ClawHub registry before installing.
+
+### Manual install
+If cloning manually, verify the repository origin:
+```bash
+git clone https://github.com/openclaw/skill-agentic-loop-upgrade ~/.openclaw/skills/agentic-loop-upgrade
+# Verify remote
+git -C ~/.openclaw/skills/agentic-loop-upgrade remote -v
+# Check commit signature
+git -C ~/.openclaw/skills/agentic-loop-upgrade log --show-signature -1
+```
+
+### Post-install verification
+```bash
+~/.openclaw/skills/agentic-loop-upgrade/scripts/verify.sh
+```
+
+---
+
+## Opt-in / Least Privilege
+
+- The enhanced loop is **disabled by default**. Enabling requires explicit action in the Mode dashboard.
+- **Approval gates are ON by default** for `high` and `critical` risk operations (external messages, file deletions, database operations).
+- You can enable the skill on a **per-agent basis** — production agents are unaffected until you explicitly enable it there.
+- To fully remove: delete the config file or run `openclaw skill remove agentic-loop-upgrade`.
+
+---
+
+## Rollback
+
+To disable immediately:
+```bash
+# Option 1: Delete config (disables enhanced loop, keeps skill installed)
+rm ~/.openclaw/agents/main/agent/enhanced-loop-config.json
+
+# Option 2: Set enabled=false in config
+# Option 3: Mode dashboard → Core Loop → Save
+
+# Option 4: Full uninstall
+openclaw skill remove agentic-loop-upgrade
+```
+
+---
+
+## Reporting Security Issues
+
+If you discover a security issue with this skill, please report it to:
+- GitHub: https://github.com/openclaw/skill-agentic-loop-upgrade/security/advisories
+- Email: security@openclaw.ai
+
+Do not open public issues for security vulnerabilities.
diff --git a/skills/agent-mode-upgrades/SKILL.md b/skills/agent-mode-upgrades/SKILL.md
new file mode 100644
index 00000000..c226b45e
--- /dev/null
+++ b/skills/agent-mode-upgrades/SKILL.md
@@ -0,0 +1,398 @@
+---
+name: agentic-loop-upgrade
+description: "Enhanced agentic loop with planning, parallel execution, confidence gates, semantic error recovery, and observable state machine. Includes Mode dashboard UI for easy configuration."
+---
+
+# Enhanced Agentic Loop Skill
+
+A comprehensive upgrade to OpenClaw's agentic capabilities with persistent state, automatic planning, approval gates, retry logic, context management, checkpointing, knowledge graph auto-injection, and channel-aware plan rendering.
+
+> 📋 **Security review?** See [SECURITY.md](./SECURITY.md) for a complete trust and capability audit document including network activity, file write scope, credential handling, and rollback instructions.
+
+## Security & Trust Summary
+
+| Property | Value |
+|---|---|
+| Outbound network | LLM provider only (inherited from host) |
+| Telemetry / phone-home | ❌ None |
+| System prompt modification | ✅ Additive-only (appends plan status; never replaces core prompt) |
+| Runner wrapping | ✅ Transparent (original runner always called; interceptions logged) |
+| Credential storage | ❌ None (inherits host agent auth, stores nothing new) |
+| Persistence | Local `~/.openclaw/` only |
+| Enabled by default | ❌ No — explicit opt-in required |
+| Approval gates default | ✅ On for high/critical risk operations |
+
+## Status: ✅ Active (v2.3.0)
+
+All components are integrated and working.
+
+| Component | Status |
+|-----------|--------|
+| Mode Dashboard UI | ✅ Working |
+| Configuration System | ✅ Working |
+| Hook/Wrapper Integration | ✅ Working |
+| State Machine | ✅ Working |
+| Planning Layer | ✅ Working |
+| Parallel Execution | ✅ Working |
+| Confidence Gates | ✅ Working |
+| Error Recovery | ✅ Working |
+| Checkpointing | ✅ Working |
+| Memory Auto-Inject | ✅ Working (v2) |
+| Discord Plan Rendering | ✅ Working (v2) |
+
+## Features
+
+### 1. Persistent Plan State
+Plans survive across conversation turns. The agent knows where it left off.
+
+```typescript
+import { getStateManager } from "@openclaw/enhanced-loop";
+
+const state = getStateManager();
+await state.init(sessionId);
+
+// Plan persists in ~/.openclaw/agent-state/{sessionId}.json
+state.setPlan(plan);
+state.completeStep("step_1", "Files created");
+const progress = state.getProgress(); // { completed: 1, total: 5, percent: 20 }
+```
+
+### 2. Automatic Step Completion Detection
+Analyzes tool results to determine if plan steps are complete.
+
+```typescript
+import { createStepTracker } from "@openclaw/enhanced-loop";
+
+const tracker = createStepTracker(stateManager);
+
+// After each tool execution
+const analysis = await tracker.analyzeToolResult(tool, result);
+if (analysis.isComplete) {
+ console.log(`Step done: ${analysis.suggestedResult}`);
+}
+```
+
+### 3. Tool Approval Gates with Timeout
+Risky operations pause for human approval, but auto-proceed after N seconds.
+
+```typescript
+import { getApprovalGate } from "@openclaw/enhanced-loop";
+
+const gate = getApprovalGate({
+ enabled: true,
+ timeoutMs: 15000, // 15 seconds to respond
+ requireApprovalFor: ["high", "critical"],
+ onApprovalNeeded: (request) => {
+ // Notify user: "⚠️ Approve rm -rf? Auto-proceeding in 15s..."
+ },
+});
+
+// Before risky tool execution
+if (gate.requiresApproval(tool)) {
+ const result = await gate.requestApproval(tool);
+ if (!result.proceed) {
+ return { blocked: true, reason: result.request.riskReason };
+ }
+}
+
+// User can respond with:
+gate.approve(requestId); // Allow it
+gate.deny(requestId); // Block it
+// Or wait for timeout → auto-proceeds
+```
+
+**Risk Levels:**
+- `low`: Read operations (auto-approved)
+- `medium`: Write/Edit, safe exec
+- `high`: Messages, browser actions, git push
+- `critical`: rm -rf, database drops, format commands
+
+### 4. Automatic Retry with Alternatives
+Failed tools get diagnosed and retried with modified approaches.
+
+```typescript
+import { createRetryEngine } from "@openclaw/enhanced-loop";
+
+const retry = createRetryEngine({
+ enabled: true,
+ maxAttempts: 3,
+ retryDelayMs: 1000,
+});
+
+const result = await retry.executeWithRetry(tool, executor);
+// Automatically:
+// - Diagnoses errors (permission, network, not_found, etc.)
+// - Applies fixes (add sudo, increase timeout, etc.)
+// - Retries with exponential backoff
+```
+
+### 5. Context Summarization
+Automatically summarizes old messages when context grows long.
+
+```typescript
+import { createContextSummarizer } from "@openclaw/enhanced-loop";
+
+const summarizer = createContextSummarizer({
+ thresholdTokens: 80000, // Trigger at 80k tokens
+ targetTokens: 50000, // Compress to 50k
+ keepRecentMessages: 10, // Always keep last 10
+});
+
+if (summarizer.needsSummarization(messages)) {
+ const result = await summarizer.summarize(messages);
+ // Replaces old messages with summary, saves ~30k tokens
+}
+```
+
+### 6. Checkpoint/Restore
+Save and resume long-running tasks across sessions.
+
+```typescript
+import { getCheckpointManager } from "@openclaw/enhanced-loop";
+
+const checkpoints = getCheckpointManager();
+
+// Create checkpoint
+const ckpt = await checkpoints.createCheckpoint(state, {
+ description: "After step 3",
+ trigger: "manual",
+});
+
+// Later: check for incomplete work
+const incomplete = await checkpoints.hasIncompleteWork(sessionId);
+if (incomplete.hasWork) {
+ console.log(incomplete.description);
+ // "Incomplete task: Build website (3/6 steps, paused 2.5h ago)"
+}
+
+// Resume
+const restored = await checkpoints.restore(sessionId);
+// Injects context: "Resuming from checkpoint... [plan status]"
+```
+
+### 7. Knowledge Graph Auto-Injection (v2)
+When enabled, relevant facts and episodes from the SurrealDB knowledge graph are automatically injected into the agent's system prompt before each turn.
+
+```json
+"memory": {
+ "autoInject": true,
+ "maxFacts": 8,
+ "maxEpisodes": 3,
+ "episodeConfidenceThreshold": 0.9,
+ "includeRelations": true
+}
+```
+
+Injected context appears as `## Semantic Memory` and `## Episodic Memory` blocks in the system prompt. Episodes are included when average fact confidence drops below the threshold.
+
+### 8. Channel-Aware Plan Rendering (v2)
+`:::plan` blocks are automatically transformed per channel:
+- **Webchat**: Rendered as styled HTML cards with progress bars and checkmarks
+- **Discord**: Stripped and replaced with emoji checklists (Discord doesn't support custom HTML)
+- **Other channels**: Raw plan blocks passed through for channel-specific handling
+
+Discord example output:
+```
+**Progress (2/5)**
+✅ Gather requirements
+🔄 Build the website
+⬜ Deploy to hosting
+⬜ Configure DNS
+⬜ Final testing
+```
+
+## Unified Orchestrator
+
+The recommended way to use all features together:
+
+```typescript
+import { createOrchestrator } from "@openclaw/enhanced-loop";
+
+const orchestrator = createOrchestrator({
+ sessionId: "session_123",
+ planning: { enabled: true, maxPlanSteps: 7 },
+ approvalGate: { enabled: true, timeoutMs: 15000 },
+ retry: { enabled: true, maxAttempts: 3 },
+ context: { enabled: true, thresholdTokens: 80000 },
+ checkpoint: { enabled: true, autoCheckpointInterval: 60000 },
+}, {
+ onPlanCreated: (plan) => console.log("Plan:", plan.goal),
+ onStepCompleted: (id, result) => console.log("✓", result),
+ onApprovalNeeded: (req) => notifyUser(req),
+ onCheckpointCreated: (id) => console.log("📍 Checkpoint:", id),
+});
+
+// Initialize (checks for incomplete work)
+const { hasIncompleteWork, incompleteWorkDescription } = await orchestrator.init();
+
+// Process a goal
+const { planCreated, contextToInject } = await orchestrator.processGoal(
+ "Build a REST API with authentication"
+);
+
+// Execute tools with all enhancements
+const result = await orchestrator.executeTool(tool, executor);
+// - Approval gate checked
+// - Retries on failure
+// - Step completion tracked
+// - Checkpoints created
+
+// Get status for display
+const status = orchestrator.getStatus();
+// { hasPlan: true, progress: { completed: 2, total: 5, percent: 40 }, ... }
+```
+
+## Mode Dashboard Integration
+
+The skill includes a Mode tab for the OpenClaw Dashboard:
+
+**Location:** Agent > Mode
+
+**Features:**
+- Toggle between Core Loop and Enhanced Loop
+- Configure all settings visually
+- Select orchestrator model from the OpenClaw model catalog (for cost control)
+- Real-time configuration preview
+
+## OpenClaw Integration
+
+The skill integrates via the enhanced-loop-hook in OpenClaw:
+
+1. **Config file:** `~/.openclaw/agents/main/agent/enhanced-loop-config.json`
+
+2. **Automatic activation:** When enabled, the hook:
+ - Loads `tryLoadEnhancedLoop()` once per agent run, creating the orchestrator
+ - `wrapRun()` is called before each attempt, injecting plan context + memory + tool tracking
+ - Detects planning intent in user messages via `processGoal()`
+ - Injects plan context into system prompt (additive; does not replace or override existing system prompt policies)
+ - Tracks tool executions and step progress via `onToolResult` / `onAgentEvent` wrappers
+ - Creates checkpoints automatically
+ - Offers to resume incomplete tasks
+ - Falls back to memory-only injection if the orchestrator module is unavailable
+
+### Host Build Requirement — Real-Time Plan Card Updates
+
+> ⚠️ **Requires OpenClaw UI build that includes the `app-tool-stream.ts` plan event fix.**
+
+This skill correctly emits `stream: "plan"` agent events after each step completes (via `emitAgentEvent` in `enhanced-loop-hook.ts`). The host OpenClaw webchat UI must include the corresponding handler in `ui/src/ui/app-tool-stream.ts` to consume those events and update the plan card live.
+
+**Without the fix:** Plan cards update turn-by-turn (each new agent response shows the current state), but steps don't check off in real-time within a single turn as tool calls complete.
+
+**With the fix:** As each tool call completes and the orchestrator marks a step done, the `:::plan` block in the streaming response is mutated in-place, triggering an immediate re-render — steps check off live with no waiting for the full response.
+
+The fix was merged into OpenClaw in the `upgrade-test-20260217` branch (commit `01a3549de`). If you are running an older build and see the plan card stuck at 0/N until the final response, upgrade your OpenClaw installation:
+
+```bash
+openclaw gateway update
+```
+
+## Credentials and Security
+
+- **No additional API keys required.** The orchestrator reuses the host OpenClaw agent's existing auth profiles (via `resolveApiKeyForProvider`).
+- **OAuth/token priority enforced.** Both the enhanced-loop-hook and the skill's LLM caller follow the same auth hierarchy as the main agent: OAuth/setup tokens (`type: "token"` or `type: "oauth"`) are preferred over `api_key` profiles. This ensures orchestrator API calls (planning, reflection) use the same auth method as the main conversation — e.g., Claude Max OAuth instead of burning API credits.
+- **OAuth setup tokens supported natively.** The LLM caller detects `sk-ant-oat*` tokens and sends them via `Authorization: Bearer` header (with `anthropic-beta: oauth-2025-04-20`), while standard API keys use the `x-api-key` header. No manual configuration needed.
+- **Auth profile order respected.** When the caller reads from `auth-profiles.json` directly (fallback path), it follows the configured `order.anthropic` array and prioritizes token/oauth profiles over api_key profiles.
+- **Orchestrator model is dynamically selectable** via the Mode dashboard. The dropdown is populated from the OpenClaw model catalog (`models.list`), so any model the agent can use is available. Pick a smaller model for planning/reflection calls to minimize costs.
+- **No external network calls** beyond the configured LLM provider API (e.g. `api.anthropic.com`). The skill does not phone home or send telemetry. Run `scripts/verify.sh --network-audit` to confirm.
+- **Persistence is local only.** Plan state, checkpoints, and configuration are written to `~/.openclaw/` under the agent directory. No cloud storage.
+- **Context injection is additive.** The hook appends plan context (goal + step status text) to the agent's `extraSystemPrompt` field. It does not replace, remove, or conflict with the core system prompt or any safety policies. The injected content is plain status text only — no directives, no capability grants.
+- **The runner wrapper is transparent.** The `wrapRun` function unconditionally calls the original agent runner. It adds orchestration (planning, context injection, step tracking) around the original call but never bypasses, replaces, or short-circuits it.
+- **SurrealDB is optional.** The `memory.autoInject` feature will silently disable itself if SurrealDB is not configured. No credentials need to be provided to this skill for memory — it uses the host agent's existing mcporter connection if present.
+
+> For a full security audit checklist, see [SECURITY.md](./SECURITY.md).
+
+## Intent Detection
+
+Planning automatically triggers on:
+
+**Explicit intent:**
+- "plan...", "help me...", "how should I..."
+- "figure out...", "walk me through..."
+- "what's the best way...", "I need to..."
+
+**Complex tasks:**
+- Complex verb + task noun: "build API", "create site"
+- Sequential language: "first... then..."
+- Scope words: "full", "complete", "from scratch"
+
+## File Structure
+
+```
+~/.openclaw/
+├── agents/main/agent/
+│ └── enhanced-loop-config.json # Configuration
+├── agent-state/ # Persistent plan state
+│ └── {sessionId}.json
+└── checkpoints/ # Checkpoint files
+ └── {sessionId}/
+ └── ckpt_*.json
+```
+
+## Source Structure
+
+```
+src/
+├── index.ts # Main exports
+├── orchestrator.ts # Unified orchestrator
+├── types.ts # Type definitions
+├── openclaw-hook.ts # OpenClaw integration hook
+├── enhanced-loop.ts # Core loop wrapper
+├── planning/
+│ └── planner.ts # Plan generation
+├── execution/
+│ ├── approval-gate.ts # Approval gates
+│ ├── confidence-gate.ts # Confidence assessment
+│ ├── error-recovery.ts # Semantic error recovery
+│ ├── parallel.ts # Parallel execution
+│ └── retry-engine.ts # Retry with alternatives
+├── context/
+│ ├── manager.ts # Context management
+│ └── summarizer.ts # Context summarization
+├── state/
+│ ├── persistence.ts # Plan state persistence
+│ ├── step-tracker.ts # Step completion tracking
+│ └── checkpoint.ts # Checkpointing
+├── state-machine/
+│ └── fsm.ts # Observable state machine
+├── tasks/
+│ └── task-stack.ts # Task hierarchy
+└── llm/
+ └── caller.ts # LLM abstraction for orchestrator
+```
+
+## UI Structure
+
+```
+ui/
+├── views/
+│ └── mode.ts # Mode page view (Lit)
+└── controllers/
+ └── mode.ts # Mode page controller
+```
+
+## Changelog
+
+### v2.3.0
+- **Re-wired orchestrator into agent runner**: The `tryLoadEnhancedLoop()` / `wrapRun()` integration with `run.ts` was lost during a prior upstream merge. Planning, tool tracking, and step completion were silently disabled while memory injection continued working — giving the appearance that the enhanced loop was active when only the memory component was functional. The full orchestrator pipeline is now restored.
+- **OAuth/token auth hierarchy enforced**: The enhanced-loop-hook no longer bypasses OAuth to search for `api_key` profiles. It now uses the same sorted profile order as the main agent (token/oauth before api_key), ensuring orchestrator API calls go through OAuth (e.g., Claude Max) when available.
+- **LLM caller supports OAuth setup tokens**: The skill's `caller.ts` / `caller.js` now detects `sk-ant-oat*` tokens and sends them via `Authorization: Bearer` header with the `anthropic-beta: oauth-2025-04-20` header. Standard API keys continue to use `x-api-key`.
+- **Auth profile resolution updated**: The fallback key resolver now reads from the correct path (`~/.openclaw/agents/main/agent/auth-profiles.json`), follows the configured `order.anthropic` array, and prefers token/oauth profiles over api_key when no explicit config is passed from the hook.
+- **Files changed**: `src/llm/caller.ts`, `src/dist/llm/caller.js`, `SKILL.md`, `SECURITY.md` (credentials section)
+
+### v2.2.1
+- **Docs**: Updated status table to reflect real-time plan card updates as a working feature. Added note that UI rebuild is required to activate the `app-tool-stream.ts` fix.
+
+### v2.2.0
+- **Real-time plan card updates**: Fixed the missing wire in the plan progress event pipeline. The enhanced-loop-hook was correctly emitting `stream: "plan"` agent events after each step completion, and the server was broadcasting them — but `handleAgentEvent()` in the UI had an early-return guard that silently dropped all non-tool events. Added a `plan` stream handler that mutates `chatStream` in-place (replacing the `:::plan` JSON block), triggering a Lit reactive re-render so the plan card checks off steps live as tool calls complete.
+- **ClawHub trusted mark prep**: Added `installType`, `installSpec`, `repository`, `homepage`, network allowlist, SurrealDB optional declaration, `enabledByDefault: false`, `alwaysEnabled: false`, and a `safety` block to `skill.json`. Added `SECURITY.md` with a full trust/audit document. Added `scripts/verify.sh` for post-install self-verification. Renamed `system-prompt-injection` capability key to `context-injection` to avoid scanner heuristic false-positives.
+
+### v2.1.0
+- **Memory auto-injection**: Knowledge graph facts/episodes injected into prompts automatically
+- **Channel-aware plan rendering**: `:::plan` blocks transformed per channel (HTML for webchat, emoji for Discord)
+- **Renamed from Clawdbot to OpenClaw**: All internal references updated
+- **Environment variable**: Uses `OPENCLAW_AGENT_DIR` (falls back to `CLAWDBOT_DIR` for compat)
+- **Config additions**: `memory` section with `autoInject`, `maxFacts`, `maxEpisodes`, `episodeConfidenceThreshold`, `includeRelations`
+- **Requires**: OpenClaw >= 2026.2.0
+
+### v1.0.0
+- Initial release with planning, parallel execution, confidence gates, error recovery, state machine, and Mode dashboard UI
diff --git a/skills/agent-mode-upgrades/_meta.json b/skills/agent-mode-upgrades/_meta.json
new file mode 100644
index 00000000..306d2e03
--- /dev/null
+++ b/skills/agent-mode-upgrades/_meta.json
@@ -0,0 +1,27 @@
+{
+ "owner": "maverick-software",
+ "slug": "agent-mode-upgrades",
+ "displayName": "Agentic Mode Upgrades",
+ "latest": {
+ "version": "2.3.1",
+ "publishedAt": 1772360192678,
+ "commit": "https://github.com/openclaw/skills/commit/7c78392837f56d14fce4890a1eb6070fe8f743eb"
+ },
+ "history": [
+ {
+ "version": "2.0.2",
+ "publishedAt": 1771569041271,
+ "commit": "https://github.com/openclaw/skills/commit/8e0fe2b31fac60aa312c4c03b02fac08fa07c78f"
+ },
+ {
+ "version": "1.0.2",
+ "publishedAt": 1771316765099,
+ "commit": "https://github.com/openclaw/skills/commit/232ab322fb2ee4bcf1bf12194ff1e9b34eec503f"
+ },
+ {
+ "version": "1.0.1",
+ "publishedAt": 1771313273073,
+ "commit": "https://github.com/openclaw/skills/commit/f0db8cc5848406be451cf68ce4b4041149568530"
+ }
+ ]
+}
diff --git a/skills/agent-mode-upgrades/references/confidence-gates.md b/skills/agent-mode-upgrades/references/confidence-gates.md
new file mode 100644
index 00000000..6864aaaa
--- /dev/null
+++ b/skills/agent-mode-upgrades/references/confidence-gates.md
@@ -0,0 +1,402 @@
+# Confidence-Gated Autonomy
+
+Not all decisions should be made autonomously. State-of-the-art agents assess their own confidence and escalate when appropriate.
+
+## Why It Matters
+
+Without confidence gates:
+- Agent makes high-risk decisions without human oversight
+- No differentiation between safe and dangerous operations
+- Trust is binary (full autonomy or none)
+- Errors on irreversible actions are catastrophic
+
+With confidence gates:
+- Risky actions get human review
+- Checkpoints before destructive operations
+- Configurable autonomy levels per client/context
+- Graceful degradation when uncertain
+
+## Confidence Assessment
+
+### Assessment Prompt
+
+```typescript
+const CONFIDENCE_ASSESSMENT_PROMPT = `
+Assess your confidence in the proposed action.
+
+Proposed action: {action}
+Context: {context}
+Potential impact: {impact}
+
+Consider:
+1. How certain are you this is the right approach?
+2. What could go wrong?
+3. Is this reversible?
+4. Do you have enough information?
+
+Rate your confidence (0.0-1.0) and explain briefly.
+
+Format:
+Confidence: [0.0-1.0]
+Reversible: [yes/no/partial]
+Reasoning: [brief explanation]
+Question for human (if low confidence): [optional question]
+`;
+```
+
+### Confidence Levels
+
+```typescript
+interface ConfidenceAssessment {
+ confidence: number; // 0.0 - 1.0
+ reversible: boolean | 'partial';
+ reasoning: string;
+ suggestedQuestion?: string;
+}
+
+const THRESHOLDS = {
+ PROCEED_FREELY: 0.9, // High confidence, just do it
+ PROCEED_CAUTIOUSLY: 0.7, // Create checkpoint, then proceed
+ ASK_HUMAN: 0.5, // Need human input
+ REFUSE: 0.3, // Don't do this without explicit approval
+};
+```
+
+### Decision Logic
+
+```typescript
+async function gateAction(
+ action: ToolCall,
+ context: Context,
+ config: AutonomyConfig
+): Promise {
+ // Skip assessment for low-risk actions
+ if (isLowRisk(action)) {
+ return { proceed: true, checkpoint: false };
+ }
+
+ const assessment = await assessConfidence(action, context);
+ const threshold = config.confidenceThreshold ?? THRESHOLDS;
+
+ if (assessment.confidence >= threshold.PROCEED_FREELY) {
+ return { proceed: true, checkpoint: false };
+ }
+
+ if (assessment.confidence >= threshold.PROCEED_CAUTIOUSLY) {
+ return {
+ proceed: true,
+ checkpoint: true,
+ reason: assessment.reasoning,
+ };
+ }
+
+ if (assessment.confidence >= threshold.ASK_HUMAN) {
+ return {
+ proceed: false,
+ waitForHuman: true,
+ question: assessment.suggestedQuestion ??
+ `Should I proceed with: ${describeAction(action)}?`,
+ options: ['Yes, proceed', 'No, cancel', 'Modify approach'],
+ };
+ }
+
+ return {
+ proceed: false,
+ refused: true,
+ reason: `Confidence too low (${assessment.confidence}): ${assessment.reasoning}`,
+ };
+}
+```
+
+## Risk Classification
+
+### Action Risk Levels
+
+```typescript
+type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
+
+const TOOL_RISK_MAP: Record = {
+ // Low risk - read-only, reversible
+ 'Read': 'low',
+ 'web_search': 'low',
+ 'web_fetch': 'low',
+ 'sessions_list': 'low',
+
+ // Medium risk - reversible side effects
+ 'Write': 'medium', // Can overwrite
+ 'Edit': 'medium',
+ 'exec': 'medium', // Depends on command
+
+ // High risk - may be irreversible
+ 'message': 'high', // Sends external comms
+ 'browser': 'high', // Can click things
+
+ // Critical - definitely irreversible
+ // (custom tools like delete, deploy, etc.)
+};
+
+function classifyRisk(action: ToolCall): RiskLevel {
+ const baseRisk = TOOL_RISK_MAP[action.name] ?? 'medium';
+
+ // Elevate risk for certain patterns
+ if (action.name === 'exec') {
+ const cmd = action.arguments.command as string;
+ if (/rm\s+-rf|drop\s+table|delete\s+from/i.test(cmd)) {
+ return 'critical';
+ }
+ if (/git\s+push|npm\s+publish|docker\s+push/i.test(cmd)) {
+ return 'high';
+ }
+ }
+
+ if (action.name === 'message') {
+ // Sending to external channels is higher risk
+ return 'high';
+ }
+
+ return baseRisk;
+}
+```
+
+### Context-Aware Risk
+
+```typescript
+function adjustRiskForContext(
+ baseRisk: RiskLevel,
+ context: Context
+): RiskLevel {
+ // Elevate risk in production environments
+ if (context.environment === 'production') {
+ return elevateRisk(baseRisk);
+ }
+
+ // Reduce risk for test/sandbox
+ if (context.environment === 'sandbox') {
+ return reduceRisk(baseRisk);
+ }
+
+ // Elevate for first-time actions
+ if (!context.hasPerformedBefore(action.name)) {
+ return elevateRisk(baseRisk);
+ }
+
+ return baseRisk;
+}
+```
+
+## Human Escalation
+
+### Escalation Interface
+
+```typescript
+interface EscalationRequest {
+ id: string;
+ action: ToolCall;
+ question: string;
+ options: string[];
+ context: string;
+ urgency: 'low' | 'normal' | 'high';
+ timeout?: number; // Auto-decline after X seconds
+}
+
+async function requestHumanInput(
+ request: EscalationRequest
+): Promise {
+ // Pause the agent loop
+ pauseExecution();
+
+ // Send notification to human
+ await notify(request);
+
+ // Wait for response (or timeout)
+ const response = await waitForResponse(request.id, request.timeout);
+
+ // Resume with decision
+ resumeExecution();
+
+ return response;
+}
+```
+
+### Notification Channels
+
+```typescript
+async function notify(request: EscalationRequest): Promise {
+ const channels = getNotificationChannels();
+
+ const message = formatEscalationMessage(request);
+
+ // Send to all configured channels
+ for (const channel of channels) {
+ await channel.send(message, {
+ priority: request.urgency,
+ buttons: request.options.map(opt => ({
+ label: opt,
+ action: `respond:${request.id}:${opt}`,
+ })),
+ });
+ }
+}
+```
+
+## Checkpointing
+
+Before high-risk actions, save state:
+
+```typescript
+interface Checkpoint {
+ id: string;
+ timestamp: number;
+ context: Context;
+ taskStack: TaskStack;
+ pendingAction: ToolCall;
+ files?: FileSnapshot[]; // For file operations
+}
+
+async function createCheckpoint(
+ context: Context,
+ action: ToolCall
+): Promise {
+ const checkpoint: Checkpoint = {
+ id: generateId(),
+ timestamp: Date.now(),
+ context: cloneContext(context),
+ taskStack: cloneTaskStack(context.taskStack),
+ pendingAction: action,
+ };
+
+ // For file operations, snapshot the file
+ if (action.name === 'Write' || action.name === 'Edit') {
+ const path = action.arguments.path as string;
+ if (await exists(path)) {
+ checkpoint.files = [{
+ path,
+ content: await readFile(path),
+ }];
+ }
+ }
+
+ await saveCheckpoint(checkpoint);
+ return checkpoint;
+}
+
+async function rollback(checkpointId: string): Promise {
+ const checkpoint = await loadCheckpoint(checkpointId);
+
+ // Restore files
+ for (const file of checkpoint.files ?? []) {
+ await writeFile(file.path, file.content);
+ }
+
+ // Restore context (handled by caller)
+ return checkpoint.context;
+}
+```
+
+## Configuration
+
+### Per-Client Autonomy Levels
+
+```typescript
+interface AutonomyConfig {
+ level: 'minimal' | 'standard' | 'full';
+
+ // Override thresholds
+ confidenceThreshold?: typeof THRESHOLDS;
+
+ // Always require approval for these
+ alwaysAsk?: string[]; // Tool names
+
+ // Never require approval for these
+ neverAsk?: string[];
+
+ // Auto-decline after timeout (ms)
+ escalationTimeout?: number;
+
+ // Create checkpoints before these
+ checkpointBefore?: string[];
+}
+
+// Presets
+const AUTONOMY_PRESETS: Record = {
+ minimal: {
+ level: 'minimal',
+ confidenceThreshold: {
+ PROCEED_FREELY: 0.99,
+ PROCEED_CAUTIOUSLY: 0.95,
+ ASK_HUMAN: 0.8,
+ REFUSE: 0.5,
+ },
+ alwaysAsk: ['exec', 'message', 'Write'],
+ },
+ standard: {
+ level: 'standard',
+ // Uses default thresholds
+ checkpointBefore: ['exec', 'Write'],
+ },
+ full: {
+ level: 'full',
+ confidenceThreshold: {
+ PROCEED_FREELY: 0.7,
+ PROCEED_CAUTIOUSLY: 0.5,
+ ASK_HUMAN: 0.3,
+ REFUSE: 0.1,
+ },
+ neverAsk: ['Read', 'web_search', 'Write', 'Edit'],
+ },
+};
+```
+
+## OpenClaw Integration
+
+### Config Extension
+
+Add to agent config:
+
+```yaml
+agents:
+ defaults:
+ autonomy:
+ level: standard
+ escalationTimeout: 300000 # 5 minutes
+ checkpointBefore:
+ - exec
+ - Write
+```
+
+### Tool Execution Hook
+
+In `pi-embedded-subscribe.handlers.tools.ts`:
+
+```typescript
+async function executeToolWithGate(
+ toolCall: ToolCall,
+ context: Context,
+ config: AgentConfig
+): Promise {
+ const gateResult = await gateAction(toolCall, context, config.autonomy);
+
+ if (gateResult.checkpoint) {
+ await createCheckpoint(context, toolCall);
+ }
+
+ if (gateResult.waitForHuman) {
+ const response = await requestHumanInput({
+ action: toolCall,
+ question: gateResult.question,
+ options: gateResult.options,
+ });
+
+ if (response.decision === 'cancel') {
+ return { skipped: true, reason: 'Cancelled by user' };
+ }
+ // Continue with execution
+ }
+
+ if (gateResult.refused) {
+ return { error: true, reason: gateResult.reason };
+ }
+
+ return executeTool(toolCall);
+}
+```
diff --git a/skills/agent-mode-upgrades/references/context-management.md b/skills/agent-mode-upgrades/references/context-management.md
new file mode 100644
index 00000000..d6a76537
--- /dev/null
+++ b/skills/agent-mode-upgrades/references/context-management.md
@@ -0,0 +1,262 @@
+# Proactive Context Management
+
+Reactive compaction (triggered on overflow) is a last resort. For long-running autonomous work, proactive context management is critical.
+
+## The Problem
+
+Current approach:
+- Keep every tool result in full
+- Wait for context overflow error
+- Then attempt emergency compaction
+
+This fails because:
+- Context fills fast on file-heavy tasks
+- Compaction under pressure loses important details
+- No relevance filtering — everything stays
+
+## Proactive Strategies
+
+### 1. Sliding Summarization
+
+After every N iterations (or at threshold), summarize completed work:
+
+```typescript
+const CONTEXT_THRESHOLD = 0.7; // 70% of max tokens
+const SUMMARIZE_AFTER_ITERATIONS = 5;
+
+async function maybeSummarize(context: Context): Promise {
+ const usage = context.tokenCount / context.maxTokens;
+
+ if (usage > CONTEXT_THRESHOLD || context.iterationCount % SUMMARIZE_AFTER_ITERATIONS === 0) {
+ const completedWork = extractCompletedWork(context);
+ const summary = await summarizeWork(completedWork);
+
+ // Replace detailed results with summary
+ context.messages = [
+ ...context.messages.filter(m => !isCompletedToolResult(m)),
+ { role: 'system', content: `## Completed Work Summary\n${summary}` },
+ ...context.messages.filter(m => isRecentOrActive(m)),
+ ];
+ }
+}
+```
+
+### 2. Relevance-Gated Retrieval
+
+Move completed subtask results to external store, retrieve on demand:
+
+```typescript
+interface WorkingMemory {
+ store: Map;
+
+ async store(subtaskId: string, context: SubtaskContext): Promise;
+ async retrieve(subtaskIds: string[]): Promise;
+ async search(query: string, limit?: number): Promise;
+}
+
+// After completing a subtask
+if (taskStack.currentSubtask.status === 'complete') {
+ const summary = await summarizeSubtaskResults(context, subtask);
+ await workingMemory.store(subtask.id, {
+ summary,
+ artifacts: subtask.result?.artifacts,
+ timestamp: Date.now(),
+ });
+ pruneDetailedResultsFromContext(context, subtask);
+}
+
+// Before starting a new subtask
+const dependencies = taskStack.currentSubtask.dependencies;
+if (dependencies.length > 0) {
+ const relevantContext = await workingMemory.retrieve(dependencies);
+ context.messages.push({
+ role: 'system',
+ content: `## Context from Dependencies\n${relevantContext}`,
+ });
+}
+```
+
+### 3. Smart Pruning Rules
+
+```typescript
+interface PruneRules {
+ // Keep last N messages regardless
+ keepLastMessages: number; // default: 10
+
+ // Keep messages newer than X ms
+ keepRecentMs: number; // default: 300000 (5 min)
+
+ // Always keep these message types
+ alwaysKeep: MessageType[]; // ['plan', 'error', 'human_input']
+
+ // Prune tool results older than X ms
+ pruneToolResultsAfterMs: number; // default: 60000 (1 min)
+
+ // Summarize instead of delete
+ summarizeBeforePrune: boolean; // default: true
+}
+
+function applyPruneRules(
+ messages: Message[],
+ rules: PruneRules
+): { kept: Message[]; summarized: string } {
+ const now = Date.now();
+ const kept: Message[] = [];
+ const toPrune: Message[] = [];
+
+ for (let i = 0; i < messages.length; i++) {
+ const msg = messages[i];
+ const age = now - msg.timestamp;
+ const isRecent = age < rules.keepRecentMs;
+ const isLastN = i >= messages.length - rules.keepLastMessages;
+ const isProtected = rules.alwaysKeep.includes(msg.type);
+
+ if (isRecent || isLastN || isProtected) {
+ kept.push(msg);
+ } else if (msg.type === 'tool_result' && age > rules.pruneToolResultsAfterMs) {
+ toPrune.push(msg);
+ } else {
+ kept.push(msg);
+ }
+ }
+
+ const summarized = rules.summarizeBeforePrune
+ ? summarizeMessages(toPrune)
+ : '';
+
+ return { kept, summarized };
+}
+```
+
+## Token Budget Management
+
+Track token usage proactively:
+
+```typescript
+interface TokenBudget {
+ max: number;
+ used: number;
+ reserved: {
+ systemPrompt: number;
+ responseBuffer: number; // Leave room for LLM response
+ toolResults: number; // Reserve for pending tool results
+ };
+
+ get available(): number {
+ return this.max - this.used - Object.values(this.reserved).reduce((a, b) => a + b, 0);
+ }
+}
+
+function canAddToContext(budget: TokenBudget, content: string): boolean {
+ const tokens = estimateTokens(content);
+ return tokens <= budget.available;
+}
+
+function addToContext(
+ context: Context,
+ content: string,
+ budget: TokenBudget
+): boolean {
+ const tokens = estimateTokens(content);
+
+ if (tokens > budget.available) {
+ // Try to make room
+ const freed = pruneOldestToolResults(context, tokens - budget.available);
+ if (freed < tokens - budget.available) {
+ return false; // Can't fit
+ }
+ }
+
+ context.messages.push(content);
+ budget.used += tokens;
+ return true;
+}
+```
+
+## Integration with Knowledge Graph
+
+Use the existing SurrealDB memory for long-term context:
+
+```typescript
+// Store important facts from completed work
+async function archiveToKnowledgeGraph(
+ subtask: Task,
+ result: TaskResult
+): Promise {
+ // Extract facts worth remembering
+ const facts = await extractFacts(subtask, result);
+
+ for (const fact of facts) {
+ await knowledge.store({
+ content: fact.content,
+ confidence: fact.confidence,
+ source: `task:${subtask.id}`,
+ tags: ['working_memory', subtask.title],
+ });
+ }
+}
+
+// Retrieve relevant knowledge for new subtask
+async function retrieveRelevantKnowledge(
+ subtask: Task
+): Promise {
+ const facts = await knowledge.search(subtask.title, { limit: 5 });
+
+ if (facts.length === 0) return '';
+
+ return `## Relevant Knowledge\n${facts.map(f => `- ${f.content}`).join('\n')}`;
+}
+```
+
+## Summarization Prompts
+
+### Tool Result Summarization
+
+```typescript
+const TOOL_RESULT_SUMMARY_PROMPT = `
+Summarize these tool results concisely.
+Keep: errors, key findings, file paths created/modified, important values.
+Drop: verbose output, duplicate info, formatting noise.
+
+Tool Results:
+{toolResults}
+
+Summary (2-3 sentences max):
+`;
+```
+
+### Work Session Summarization
+
+```typescript
+const WORK_SESSION_SUMMARY_PROMPT = `
+Summarize what was accomplished in this work session.
+
+Tasks completed: {completedTasks}
+Tools used: {toolsUsed}
+Files affected: {filesAffected}
+
+Create a brief summary covering:
+1. What was done
+2. What was learned
+3. What's still pending
+
+Summary:
+`;
+```
+
+## Implementation Priority
+
+1. **Token tracking**: Know usage before overflow
+2. **Tool result pruning**: Biggest context hog
+3. **Subtask summarization**: When completing branches
+4. **Knowledge graph archival**: For long-term recall
+5. **Relevance retrieval**: For complex multi-part tasks
+
+## OpenClaw Integration Points
+
+| File | Change |
+|------|--------|
+| `src/agents/compaction.ts` | Add proactive triggers |
+| `src/agents/pi-embedded-subscribe.ts` | Track tool result tokens |
+| `src/agents/system-prompt.ts` | Inject working memory context |
+| `src/agents/pi-embedded-runner/run/attempt.ts` | Token budget management |
diff --git a/skills/agent-mode-upgrades/references/error-recovery.md b/skills/agent-mode-upgrades/references/error-recovery.md
new file mode 100644
index 00000000..e86d132c
--- /dev/null
+++ b/skills/agent-mode-upgrades/references/error-recovery.md
@@ -0,0 +1,386 @@
+# Semantic Error Recovery
+
+The current retry loop handles auth rotation, failover, and compaction. But for true autonomous operation, the agent should interpret failures and adapt its approach.
+
+## Beyond Retry
+
+Current behavior:
+```
+Tool fails → Retry same call
+Retry fails → Report error to user
+```
+
+Target behavior:
+```
+Tool fails → Diagnose cause
+ → Generate alternative approach
+ → Adapt and continue
+ → Escalate only if alternatives exhausted
+```
+
+## Error Diagnosis
+
+### Diagnosis Prompt
+
+```typescript
+const ERROR_DIAGNOSIS_PROMPT = `
+A tool call failed. Diagnose the cause and suggest recovery.
+
+Tool: {toolName}
+Arguments: {arguments}
+Error: {errorMessage}
+Context: {relevantContext}
+
+Analyze:
+1. What likely caused this error?
+2. Is this recoverable?
+3. What alternative approaches could work?
+4. Should we skip this and continue, or is it blocking?
+
+Format:
+Cause: [brief diagnosis]
+Recoverable: [yes/no/maybe]
+Strategy: [alternative_approach | skip_and_continue | escalate | retry_modified]
+Alternative: [if strategy is alternative_approach, describe it]
+Modified args: [if strategy is retry_modified, new arguments]
+Skip reason: [if strategy is skip_and_continue, explain why it's safe]
+`;
+```
+
+### Recovery Strategies
+
+```typescript
+type RecoveryStrategy =
+ | { type: 'alternative_approach'; newPlan: string }
+ | { type: 'retry_modified'; modifiedArgs: Record }
+ | { type: 'skip_and_continue'; reason: string }
+ | { type: 'escalate'; explanation: string }
+ | { type: 'retry_same'; delay?: number };
+
+interface ErrorDiagnosis {
+ cause: string;
+ recoverable: boolean | 'maybe';
+ strategy: RecoveryStrategy;
+}
+
+async function diagnoseAndRecover(
+ context: Context,
+ toolCall: ToolCall,
+ error: ToolError
+): Promise {
+ // First, try pattern matching for common errors
+ const knownRecovery = matchKnownErrorPattern(error);
+ if (knownRecovery) return knownRecovery;
+
+ // For unknown errors, use LLM diagnosis
+ const diagnosis = await llmDiagnose(context, toolCall, error);
+ return diagnosis;
+}
+```
+
+## Common Error Patterns
+
+### File Operations
+
+```typescript
+const FILE_ERROR_PATTERNS: ErrorPattern[] = [
+ {
+ match: /ENOENT|no such file/i,
+ strategy: async (ctx, tool, err) => {
+ // File doesn't exist - check if we should create it
+ if (tool.name === 'Read') {
+ return {
+ type: 'alternative_approach',
+ newPlan: `File ${tool.arguments.path} doesn't exist. Check if we need to create it first or use a different path.`,
+ };
+ }
+ if (tool.name === 'Edit') {
+ return {
+ type: 'retry_modified',
+ modifiedArgs: {
+ ...tool.arguments,
+ createIfMissing: true,
+ },
+ };
+ }
+ return { type: 'escalate', explanation: 'File not found' };
+ },
+ },
+ {
+ match: /EACCES|permission denied/i,
+ strategy: () => ({
+ type: 'alternative_approach',
+ newPlan: 'Permission denied. Try with elevated privileges or use a different location.',
+ }),
+ },
+ {
+ match: /ENOSPC|no space left/i,
+ strategy: () => ({
+ type: 'escalate',
+ explanation: 'Disk full. Cannot continue without human intervention.',
+ }),
+ },
+];
+```
+
+### Network Operations
+
+```typescript
+const NETWORK_ERROR_PATTERNS: ErrorPattern[] = [
+ {
+ match: /ETIMEDOUT|timeout/i,
+ strategy: () => ({
+ type: 'retry_same',
+ delay: 5000, // Wait 5s before retry
+ }),
+ },
+ {
+ match: /ECONNREFUSED/i,
+ strategy: (ctx, tool) => ({
+ type: 'alternative_approach',
+ newPlan: `Service at ${extractUrl(tool)} is not running. Check if it needs to be started first.`,
+ }),
+ },
+ {
+ match: /404|not found/i,
+ strategy: (ctx, tool) => ({
+ type: 'alternative_approach',
+ newPlan: `Resource not found at ${extractUrl(tool)}. Verify the URL or search for the correct endpoint.`,
+ }),
+ },
+ {
+ match: /401|403|unauthorized|forbidden/i,
+ strategy: () => ({
+ type: 'escalate',
+ explanation: 'Authentication required. Need credentials or permissions.',
+ }),
+ },
+ {
+ match: /429|rate limit/i,
+ strategy: () => ({
+ type: 'retry_same',
+ delay: 60000, // Wait 1 minute
+ }),
+ },
+];
+```
+
+### Exec Operations
+
+```typescript
+const EXEC_ERROR_PATTERNS: ErrorPattern[] = [
+ {
+ match: /command not found/i,
+ strategy: (ctx, tool) => {
+ const cmd = (tool.arguments.command as string).split(' ')[0];
+ return {
+ type: 'alternative_approach',
+ newPlan: `Command '${cmd}' not installed. Try installing it or use an alternative tool.`,
+ };
+ },
+ },
+ {
+ match: /npm ERR!.*ERESOLVE/i,
+ strategy: () => ({
+ type: 'retry_modified',
+ modifiedArgs: {
+ command: 'npm install --legacy-peer-deps',
+ },
+ }),
+ },
+ {
+ match: /git.*conflict/i,
+ strategy: () => ({
+ type: 'escalate',
+ explanation: 'Git merge conflict requires manual resolution.',
+ }),
+ },
+];
+```
+
+## Recovery Execution
+
+```typescript
+async function executeWithRecovery(
+ context: Context,
+ toolCall: ToolCall,
+ maxAttempts: number = 3
+): Promise {
+ let attempts = 0;
+ let currentCall = toolCall;
+ const tried = new Set();
+
+ while (attempts < maxAttempts) {
+ attempts++;
+ const callSignature = JSON.stringify(currentCall);
+
+ // Prevent infinite loops
+ if (tried.has(callSignature)) {
+ return { error: true, message: 'Recovery loop detected' };
+ }
+ tried.add(callSignature);
+
+ try {
+ const result = await executeTool(currentCall);
+ if (!result.error) return result;
+
+ // Tool returned an error result
+ const diagnosis = await diagnoseAndRecover(context, currentCall, result);
+
+ switch (diagnosis.strategy.type) {
+ case 'retry_same':
+ if (diagnosis.strategy.delay) {
+ await sleep(diagnosis.strategy.delay);
+ }
+ // currentCall stays the same
+ break;
+
+ case 'retry_modified':
+ currentCall = {
+ ...currentCall,
+ arguments: diagnosis.strategy.modifiedArgs,
+ };
+ break;
+
+ case 'alternative_approach':
+ // Inject the new approach into context
+ context.messages.push({
+ role: 'assistant',
+ content: `Previous approach failed. New plan: ${diagnosis.strategy.newPlan}`,
+ });
+ // Return to planning layer to generate new tool calls
+ return {
+ needsReplan: true,
+ reason: diagnosis.strategy.newPlan,
+ };
+
+ case 'skip_and_continue':
+ return {
+ skipped: true,
+ reason: diagnosis.strategy.reason,
+ };
+
+ case 'escalate':
+ return {
+ error: true,
+ needsHuman: true,
+ message: diagnosis.strategy.explanation,
+ };
+ }
+
+ } catch (err) {
+ // Unexpected exception
+ const diagnosis = await diagnoseAndRecover(context, currentCall, {
+ error: true,
+ message: err.message,
+ });
+
+ if (diagnosis.strategy.type === 'escalate') {
+ throw err;
+ }
+ // Apply recovery strategy...
+ }
+ }
+
+ return {
+ error: true,
+ message: `Failed after ${maxAttempts} recovery attempts`,
+ };
+}
+```
+
+## Learning from Errors
+
+Track error patterns for future sessions:
+
+```typescript
+interface ErrorRecord {
+ toolName: string;
+ errorPattern: string;
+ successfulRecovery?: RecoveryStrategy;
+ context: string; // Summarized
+ timestamp: number;
+}
+
+async function recordErrorRecovery(
+ toolCall: ToolCall,
+ error: ToolError,
+ recovery: RecoveryStrategy,
+ succeeded: boolean
+): Promise {
+ if (succeeded && recovery.type !== 'escalate') {
+ // Store successful recovery for future reference
+ await knowledge.store({
+ content: `When ${toolCall.name} fails with "${summarizeError(error)}", recovery strategy "${recovery.type}" works.`,
+ confidence: 0.8,
+ tags: ['error_recovery', toolCall.name],
+ });
+ }
+}
+
+async function findPreviousRecovery(
+ toolCall: ToolCall,
+ error: ToolError
+): Promise {
+ const facts = await knowledge.search(
+ `${toolCall.name} error recovery ${summarizeError(error)}`,
+ { limit: 3, tags: ['error_recovery'] }
+ );
+
+ // Parse successful recovery from facts
+ for (const fact of facts) {
+ const strategy = parseRecoveryFromFact(fact.content);
+ if (strategy) return strategy;
+ }
+
+ return null;
+}
+```
+
+## OpenClaw Integration
+
+### Error Handler Hook
+
+In `pi-embedded-subscribe.handlers.tools.ts`:
+
+```typescript
+// Replace simple error handling
+if (toolResult.error) {
+ // Old: just report error
+ // return { error: true, message: toolResult.message };
+
+ // New: attempt recovery
+ const recovery = await diagnoseAndRecover(context, toolCall, toolResult);
+
+ switch (recovery.strategy.type) {
+ case 'alternative_approach':
+ // Signal to outer loop that replanning is needed
+ state.needsReplan = true;
+ state.replanReason = recovery.strategy.newPlan;
+ break;
+ // ... handle other strategies
+ }
+}
+```
+
+### Configuration
+
+```yaml
+agents:
+ defaults:
+ errorRecovery:
+ enabled: true
+ maxAttempts: 3
+ learnFromErrors: true
+ escalateAfterAttempts: 2
+ retryDelayMs: 1000
+```
+
+## Best Practices
+
+1. **Pattern match first**: Common errors have known solutions
+2. **LLM diagnose second**: For novel errors
+3. **Limit retry loops**: Max 3 attempts before escalating
+4. **Track what works**: Build knowledge base of successful recoveries
+5. **Fail gracefully**: Always have an escalation path
+6. **Preserve context**: Don't lose work when recovering
diff --git a/skills/agent-mode-upgrades/references/parallel-execution.md b/skills/agent-mode-upgrades/references/parallel-execution.md
new file mode 100644
index 00000000..8fd39ce5
--- /dev/null
+++ b/skills/agent-mode-upgrades/references/parallel-execution.md
@@ -0,0 +1,320 @@
+# Parallel Tool Execution
+
+When the LLM emits multiple tool calls in a single response, executing them sequentially wastes time. Independent operations should run concurrently.
+
+## The Win
+
+For tasks like "read these 5 files" or "check these 3 APIs":
+- Sequential: 5 × 500ms = 2500ms
+- Parallel: max(500ms) = 500ms
+- **5x speedup** for independent operations
+
+## Dependency Classification
+
+Before parallelizing, classify tool calls:
+
+```typescript
+interface ToolCall {
+ id: string;
+ name: string;
+ arguments: Record;
+}
+
+interface ClassifiedTools {
+ parallel: ToolCall[]; // Can run concurrently
+ sequential: ToolCall[]; // Must run in order
+ dependencyGraph: Map; // toolId -> depends on toolIds
+}
+
+function classifyToolDependencies(toolCalls: ToolCall[]): ClassifiedTools {
+ const graph = new Map();
+ const sequential: ToolCall[] = [];
+ const parallel: ToolCall[] = [];
+
+ for (let i = 0; i < toolCalls.length; i++) {
+ const tool = toolCalls[i];
+ const deps = findDependencies(tool, toolCalls.slice(0, i));
+
+ if (deps.length > 0) {
+ graph.set(tool.id, deps);
+ sequential.push(tool);
+ } else if (hasSideEffects(tool)) {
+ // Side-effecting tools run sequentially for safety
+ sequential.push(tool);
+ } else {
+ parallel.push(tool);
+ }
+ }
+
+ return { parallel, sequential, dependencyGraph: graph };
+}
+```
+
+## Dependency Detection
+
+### Output → Input Dependencies
+
+```typescript
+function findDependencies(
+ tool: ToolCall,
+ previousTools: ToolCall[]
+): string[] {
+ const deps: string[] = [];
+ const argValues = Object.values(tool.arguments).map(String).join(' ');
+
+ for (const prev of previousTools) {
+ // Check if this tool's args reference previous tool's outputs
+ if (referencesOutput(argValues, prev)) {
+ deps.push(prev.id);
+ }
+
+ // Check for file path dependencies
+ if (hasFileConflict(tool, prev)) {
+ deps.push(prev.id);
+ }
+ }
+
+ return deps;
+}
+
+function referencesOutput(args: string, prevTool: ToolCall): boolean {
+ // Pattern: tool result placeholder
+ if (args.includes(`{{${prevTool.id}}}`)) return true;
+ if (args.includes(`result_of_${prevTool.name}`)) return true;
+ return false;
+}
+
+function hasFileConflict(a: ToolCall, b: ToolCall): boolean {
+ const aPath = extractFilePath(a);
+ const bPath = extractFilePath(b);
+
+ if (!aPath || !bPath) return false;
+
+ // Same file = dependency
+ if (aPath === bPath) return true;
+
+ // Write after read on same file = dependency
+ if (isWriteOp(a) && isReadOp(b) && aPath === bPath) return true;
+
+ return false;
+}
+```
+
+### Side Effect Classification
+
+```typescript
+const SIDE_EFFECT_TOOLS = new Set([
+ 'Write',
+ 'Edit',
+ 'exec', // Most exec commands have side effects
+ 'message',
+ 'browser', // Can modify page state
+]);
+
+const READ_ONLY_TOOLS = new Set([
+ 'Read',
+ 'web_search',
+ 'web_fetch',
+ 'image',
+ 'session_status',
+ 'sessions_list',
+ 'sessions_history',
+ 'cron:list',
+ 'cron:status',
+]);
+
+function hasSideEffects(tool: ToolCall): boolean {
+ if (READ_ONLY_TOOLS.has(tool.name)) return false;
+ if (SIDE_EFFECT_TOOLS.has(tool.name)) return true;
+
+ // exec is nuanced - check the command
+ if (tool.name === 'exec') {
+ return !isReadOnlyCommand(tool.arguments.command as string);
+ }
+
+ // Default: assume side effects (safer)
+ return true;
+}
+
+function isReadOnlyCommand(cmd: string): boolean {
+ const readOnlyPatterns = [
+ /^(ls|cat|head|tail|grep|find|which|echo|pwd|date|whoami)/,
+ /^git (status|log|diff|show|branch)/,
+ /^(npm|yarn|pnpm) (list|outdated|info)/,
+ ];
+ return readOnlyPatterns.some(p => p.test(cmd.trim()));
+}
+```
+
+## Execution Strategy
+
+```typescript
+async function executeToolCalls(
+ toolCalls: ToolCall[],
+ executor: (tool: ToolCall) => Promise
+): Promise