Token导航 LogoToken导航TokenDH.com
前端设计需要联网github未标认证来源可访问许可证需确认审计提醒

idempotencyidempotency 测试

Agent Skill

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

总安装

569

周安装

23

GitHub Stars

777

下载量

178
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 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

简介

idempotency 用于安全处理重试与幂等操作,适合在 Codex、Claude、Cursor、Gemini CLI 中需要防止支付重复、订单重复创建时使用。

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

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

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

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 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills