Token导航 LogoToken导航

Agent Skill · 前端设计

idempotency(idempotency 测试)

idempotency 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

idempotency 是前端设计类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证信息还不明确,第三方扫描有提醒项,按安装量在前端设计类的 10,216 个 Skill 里排第 5,987。

总安装 569周安装 23GitHub Stars 777下载 178核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:idempotency(idempotency 测试)
来源仓库:https://github.com/dadbodgeoff/drift
仓库路径:skills/idempotency
安装命令:
npx skills add https://github.com/dadbodgeoff/drift --skill idempotency
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/dadbodgeoff/drift --skill idempotency

这个 Skill 是做什么的

前端设计类 Skill 关注页面结构、视觉规范和组件写法,输出通常是可运行的界面代码。

idempotency 用于安全处理重试与幂等操作,适合在 Codex、Claude、Cursor、Gemini CLI 中需要防止支付重复、订单重复创建时使用。它通过唯一键与状态机确保多次执行结果一致,适用于 webhook 与客户端重试场景。

使用时需设计唯一标识生成与状态查询机制,避免竞态条件;建议记录操作历史供审计。安装前请确认数据存储方案,注意是否会引入额外查询开销,确保幂等逻辑高效可靠。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

来源仓库内路径:skills/idempotency

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/dadbodgeoff/drift --skill idempotency;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

idempotency 怎么安装?

在终端执行 npx skills add https://github.com/dadbodgeoff/drift --skill idempotency。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

idempotency 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

idempotency 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

idempotency 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

idempotency 用的人多吗?

公开数据是累计安装 569 次,近一周安装 23 次,下载 178 次,GitHub 777 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

idempotency 安全吗?

第三方扫描有提醒项,说明存在需要人工确认的行为。安装前请打开源码看清它读了什么、联网到哪里。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理前端设计类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

前端设计类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
frontend-design

前端设计 · Codex、Claude

8,823,360126,072只读MIT通过
vercel-composition-patterns

Vercel 组合模式 · Codex、Claude

3,858,81625,893需要联网MIT通过
vercel-react-native-skills

Vercel React native skills 前端 · Codex、Claude

2,468,66425,878只读MIT通过
adapt

adapt 搜索 · Codex、Claude

2,027,04023,125操作浏览器未明确通过
bolder

bolder 分析 · Codex、Claude

2,004,28823,130只读未明确通过
idempotency

idempotency 测试 · 本页

569777需要联网未明确有提醒

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Idempotent API Operations

Safely handle retries without duplicate side effects.

When to Use This Skill

  • Payment processing (charges, refunds)
  • Order creation and fulfillment
  • Any operation with side effects
  • APIs that may be retried by clients
  • Webhook handlers

What is Idempotency?

An operation is idempotent if executing it multiple times produces the same result as executing it once.

Request 1: POST /orders {item: "book"}  → Order #123 created
Request 2: POST /orders {item: "book"}  → Order #123 returned (not #124)
                                          (same idempotency key)

Architecture

┌─────────────────────────────────────────────────────┐
│                    Client Request                    │
│              Idempotency-Key: abc-123               │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│              Check Idempotency Store                │
│                                                     │
│  Key exists?                                        │
│  ├─ Yes, completed → Return cached response         │
│  ├─ Yes, in-progress → Return 409 Conflict          │
│  └─ No → Continue processing                        │
└─────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────┐
│              Lock & Process Request                 │
│                                                     │
│  1. Acquire lock (set key as "processing")          │
│  2. Execute operation                               │
│  3. Store response                                  │
│  4. Return response                                 │
└─────────────────────────────────────────────────────┘

TypeScript Implementation

Idempotency Store

// idempotency-store.ts
import { Redis } from 'ioredis';

interface IdempotencyRecord {
  status: 'processing' | 'completed';
  response?: {
    statusCode: number;
    body: unknown;
    headers?: Record<string, string>;
  };
  createdAt: number;
  completedAt?: number;
}

interface IdempotencyConfig {
  redis: Redis;
  keyPrefix?: string;
  lockTtlMs?: number;      // How long to hold processing lock
  responseTtlMs?: number;  // How long to cache completed responses
}

class IdempotencyStore {
  private redis: Redis;
  private keyPrefix: string;
  private lockTtl: number;
  private responseTtl: number;

  constructor(config: IdempotencyConfig) {
    this.redis = config.redis;
    this.keyPrefix = config.keyPrefix || 'idempotency:';
    this.lockTtl = config.lockTtlMs || 60000;      // 1 minute
    this.responseTtl = config.responseTtlMs || 86400000; // 24 hours
  }

  async get(key: string): Promise<IdempotencyRecord | null> {
    const data = await this.redis.get(this.keyPrefix + key);
    return data ? JSON.parse(data) : null;
  }

  async acquireLock(key: string): Promise<boolean> {
    const record: IdempotencyRecord = {
      status: 'processing',
      createdAt: Date.now(),
    };

    // SET NX = only set if not exists
    const result = await this.redis.set(
      this.keyPrefix + key,
      JSON.stringify(record),
      'PX',
      this.lockTtl,
      'NX'
    );

    return result === 'OK';
  }

  async complete(
    key: string,
    response: IdempotencyRecord['response']
  ): Promise<void> {
    const record: IdempotencyRecord = {
      status: 'completed',
      response,
      createdAt: Date.now(),
      completedAt: Date.now(),
    };

    await this.redis.set(
      this.keyPrefix + key,
      JSON.stringify(record),
      'PX',
      this.responseTtl
    );
  }

  async release(key: string): Promise<void> {
    await this.redis.del(this.keyPrefix + key);
  }
}

export { IdempotencyStore, IdempotencyRecord, IdempotencyConfig };

Express Middleware

// idempotency-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { IdempotencyStore } from './idempotency-store';

interface IdempotencyOptions {
  store: IdempotencyStore;
  headerName?: string;
  methods?: string[];
  paths?: RegExp[];
}

function idempotencyMiddleware(options: IdempotencyOptions) {
  const {
    store,
    headerName = 'Idempotency-Key',
    methods = ['POST', 'PUT', 'PATCH'],
    paths = [/.*/],
  } = options;

  return async (req: Request, res: Response, next: NextFunction) => {
    // Only apply to specified methods
    if (!methods.includes(req.method)) {
      return next();
    }

    // Only apply to specified paths
    if (!paths.some(p => p.test(req.path))) {
      return next();
    }

    const idempotencyKey = req.headers[headerName.toLowerCase()] as string;

    // No key provided - proceed without idempotency
    if (!idempotencyKey) {
      return next();
    }

    // Create a unique key combining the idempotency key with request details
    const fullKey = `${req.method}:${req.path}:${idempotencyKey}`;

    // Check for existing record
    const existing = await store.get(fullKey);

    if (existing) {
      if (existing.status === 'processing') {
        // Request is still being processed
        return res.status(409).json({
          error: 'Conflict',
          message: 'A request with this idempotency key is already being processed',
        });
      }

      if (existing.status === 'completed' && existing.response) {
        // Return cached response
        res.status(existing.response.statusCode);
        if (existing.response.headers) {
          for (const [key, value] of Object.entries(existing.response.headers)) {
            res.setHeader(key, value);
          }
        }
        res.setHeader('X-Idempotent-Replayed', 'true');
        return res.json(existing.response.body);
      }
    }

    // Try to acquire lock
    const acquired = await store.acquireLock(fullKey);

    if (!acquired) {
      // Another request just acquired the lock
      return res.status(409).json({
        error: 'Conflict',
        message: 'A request with this idempotency key is already being processed',
      });
    }

    // Capture the response
    const originalJson = res.json.bind(res);
    let responseBody: unknown;

    res.json = (body: unknown) => {
      responseBody = body;
      return originalJson(body);
    };

    // Store response after it's sent
    res.on('finish', async () => {
      if (res.statusCode >= 200 && res.statusCode < 500) {
        // Store successful responses and client errors (but not server errors)
        await store.complete(fullKey, {
          statusCode: res.statusCode,
          body: responseBody,
        });
      } else {
        // Release lock for server errors (allow retry)
        await store.release(fullKey);
      }
    });

    next();
  };
}

export { idempotencyMiddleware, IdempotencyOptions };

Usage

// app.ts
import express from 'express';
import { Redis } from 'ioredis';
import { IdempotencyStore } from './idempotency-store';
import { idempotencyMiddleware } from './idempotency-middleware';

const app = express();
const redis = new Redis();

const idempotencyStore = new IdempotencyStore({ redis });

// Apply to all POST/PUT/PATCH requests
app.use(idempotencyMiddleware({
  store: idempotencyStore,
  methods: ['POST', 'PUT', 'PATCH'],
}));

// Or apply to specific routes
app.post('/orders',
  idempotencyMiddleware({
    store: idempotencyStore,
    paths: [/^\/orders$/],
  }),
  async (req, res) => {
    const order = await createOrder(req.body);
    res.status(201).json(order);
  }
);

Python Implementation

# idempotency.py
import json
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
from functools import wraps

@dataclass
class IdempotencyRecord:
    status: str  # 'processing' | 'completed'
    response: Optional[Dict[str, Any]] = None
    created_at: float = 0
    completed_at: Optional[float] = None

class IdempotencyStore:
    def __init__(
        self,
        redis_client: redis.Redis,
        key_prefix: str = "idempotency:",
        lock_ttl_ms: int = 60000,
        response_ttl_ms: int = 86400000,
    ):
        self.redis = redis_client
        self.key_prefix = key_prefix
        self.lock_ttl = lock_ttl_ms
        self.response_ttl = response_ttl_ms

    def get(self, key: str) -> Optional[IdempotencyRecord]:
        data = self.redis.get(self.key_prefix + key)
        if not data:
            return None
        parsed = json.loads(data)
        return IdempotencyRecord(**parsed)

    def acquire_lock(self, key: str) -> bool:
        record = {
            "status": "processing",
            "created_at": time.time(),
        }
        result = self.redis.set(
            self.key_prefix + key,
            json.dumps(record),
            px=self.lock_ttl,
            nx=True,
        )
        return result is True

    def complete(self, key: str, response: Dict[str, Any]) -> None:
        record = {
            "status": "completed",
            "response": response,
            "created_at": time.time(),
            "completed_at": time.time(),
        }
        self.redis.set(
            self.key_prefix + key,
            json.dumps(record),
            px=self.response_ttl,
        )

    def release(self, key: str) -> None:
        self.redis.delete(self.key_prefix + key)

FastAPI Middleware

# fastapi_idempotency.py
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

class IdempotencyMiddleware(BaseHTTPMiddleware):
    def __init__(
        self,
        app,
        store: IdempotencyStore,
        header_name: str = "Idempotency-Key",
        methods: list = None,
    ):
        super().__init__(app)
        self.store = store
        self.header_name = header_name
        self.methods = methods or ["POST", "PUT", "PATCH"]

    async def dispatch(self, request: Request, call_next):
        if request.method not in self.methods:
            return await call_next(request)

        idempotency_key = request.headers.get(self.header_name)
        if not idempotency_key:
            return await call_next(request)

        full_key = f"{request.method}:{request.url.path}:{idempotency_key}"

        # Check existing
        existing = self.store.get(full_key)
        if existing:
            if existing.status == "processing":
                raise HTTPException(
                    status_code=409,
                    detail="Request with this idempotency key is being processed",
                )
            if existing.status == "completed" and existing.response:
                return JSONResponse(
                    content=existing.response["body"],
                    status_code=existing.response["status_code"],
                    headers={"X-Idempotent-Replayed": "true"},
                )

        # Acquire lock
        if not self.store.acquire_lock(full_key):
            raise HTTPException(
                status_code=409,
                detail="Request with this idempotency key is being processed",
            )

        try:
            response = await call_next(request)

            # Cache successful responses
            if 200 <= response.status_code < 500:
                body = b""
                async for chunk in response.body_iterator:
                    body += chunk

                self.store.complete(full_key, {
                    "status_code": response.status_code,
                    "body": json.loads(body),
                })

                return JSONResponse(
                    content=json.loads(body),
                    status_code=response.status_code,
                )
            else:
                self.store.release(full_key)
                return response
        except Exception:
            self.store.release(full_key)
            raise

Decorator Pattern

# idempotent_decorator.py
from functools import wraps

def idempotent(store: IdempotencyStore, key_func=None):
    """
    Decorator for idempotent functions.

    @idempotent(store, key_func=lambda args: args[0].order_id)
    async def process_order(order: Order):
        ...
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Generate key
            if key_func:
                key = key_func(args, kwargs)
            else:
                key = f"{func.__name__}:{hash(str(args) + str(kwargs))}"

            # Check existing
            existing = store.get(key)
            if existing and existing.status == "completed":
                return existing.response["result"]

            # Acquire lock
            if not store.acquire_lock(key):
                raise Exception("Operation already in progress")

            try:
                result = await func(*args, **kwargs)
                store.complete(key, {"result": result})
                return result
            except Exception:
                store.release(key)
                raise

        return wrapper
    return decorator

# Usage
@idempotent(store, key_func=lambda args, kwargs: f"order:{kwargs.get('order_id')}")
async def process_payment(order_id: str, amount: float):
    return await stripe.charges.create(amount=amount)

Client-Side Implementation

// idempotent-client.ts
class IdempotentClient {
  private generateKey(): string {
    return crypto.randomUUID();
  }

  async post<T>(url: string, data: unknown, options?: RequestInit): Promise<T> {
    const idempotencyKey = this.generateKey();

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
        ...options?.headers,
      },
      body: JSON.stringify(data),
      ...options,
    });

    if (response.status === 409) {
      // Request in progress, wait and retry
      await new Promise(resolve => setTimeout(resolve, 1000));
      return this.postWithKey(url, data, idempotencyKey, options);
    }

    return response.json();
  }

  private async postWithKey<T>(
    url: string,
    data: unknown,
    idempotencyKey: string,
    options?: RequestInit,
    retries = 3
  ): Promise<T> {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
        ...options?.headers,
      },
      body: JSON.stringify(data),
      ...options,
    });

    if (response.status === 409 && retries > 0) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      return this.postWithKey(url, data, idempotencyKey, options, retries - 1);
    }

    return response.json();
  }
}

Best Practices

  1. Include request details in key: Method + path + idempotency key
  2. Set appropriate TTLs: Lock TTL < Response TTL
  3. Handle 409 gracefully: Client should wait and retry
  4. Don't cache server errors: Allow retry on 5xx
  5. Use UUIDs for keys: Clients should generate unique keys

Common Mistakes

  • Using sequential IDs (collisions across users)
  • Caching server errors (prevents retry)
  • Too short response TTL (client retries get new result)
  • Not including request path in key (different endpoints collide)
  • Forgetting to release lock on error

Security Considerations

  • Validate idempotency key format
  • Rate limit by idempotency key
  • Don't expose internal state in 409 responses
  • Consider per-user key namespacing

平台分布

Codex

32.76%
按下载量换算58

Claude

28.4%
按下载量换算51

Cursor

19.96%
按下载量换算36

Gemini CLI

10.08%
按下载量换算18

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills