Token导航 LogoToken导航

Agent Skill · 研究检索

redis-patterns(Redis 模式)

用于辅助数据库表结构、查询语句、迁移脚本和数据维护任务。它适合让 Agent 分析 schema、编写 SQL、排查查询问题、整理索引或生成迁移建议。使用时需要明确数据库类型、连接环境和目标表,区分只读分析与写入变更;涉及删除、更新、迁移和批量导入时,应优先 dry-run、备份或事务保护,避免误操作。

redis-patterns 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为接触敏感数据;许可证为 MIT,第三方扫描已通过,按安装量在研究检索类的 73,675 个 Skill 里排第 22,267。

总安装 4,608周安装 192GitHub Stars 33下载 1,536核验 2026-05-01

安装说明

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

安装方式

通过对话安装

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

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

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/patricio0312rev/skills --skill redis-patterns

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

用于 Redis 数据结构和常用操作模式。适合字符串、哈希、列表等类型使用指导。redis-patterns 属于研究检索类 Skill,可作为该场景下的辅助能力补充。通过 npx 命令从 GitHub 仓库安装使用。需明确连接环境和目标表,区分只读与写入操作。建议结合原始 README 核验具体用法和功能边界。

来源仓库内路径:skills/redis-patterns

上手步骤

4 步
  1. 1

    确认自己的宿主工具

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

  2. 2

    拿到安装命令

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

  3. 3

    安装后重启宿主

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

  4. 4

    按最小权限试用

    它需要接触敏感数据,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

redis-patterns 怎么安装?

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

redis-patterns 支持哪些 AI 工具?

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

redis-patterns 需要什么权限?

权限级别是接触敏感数据。该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

redis-patterns 是免费的吗?

来源标注的许可证是 MIT。本站不代收任何费用,也不托管安装包;能否商用、要不要署名,以许可证原文为准。

redis-patterns 用的人多吗?

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

redis-patterns 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

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

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 3 个目录同时收录,说明这个 Skill 流传比较广。
  • 许可证明确为 MIT,商用和分发边界好判断。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 需要接触敏感数据,装到常用环境前要先评估影响面。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。
  • 需要许可证清楚、并且有第三方扫描结果的团队项目。

不太适合

  • 环境里有大量密钥、凭证,不方便给第三方 Skill 读取的人。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

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

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
redis-patterns

Redis 模式 · 本页

4,60833接触敏感数据MIT通过

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

SKILL.md 原文

Redis Patterns

Implement common Redis patterns for high-performance applications.

Core Workflow

  1. Setup connection: Configure Redis client
  2. Choose pattern: Caching, sessions, queues, etc.
  3. Implement operations: CRUD with proper TTL
  4. Handle errors: Reconnection, fallbacks
  5. Monitor performance: Memory, latency
  6. Optimize: Pipelining, clustering

Connection Setup

// redis/client.ts
import { Redis } from 'ioredis';

// Single instance
export const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  db: parseInt(process.env.REDIS_DB || '0'),

  // Connection options
  maxRetriesPerRequest: 3,
  retryStrategy(times) {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },

  // Performance options
  enableReadyCheck: true,
  enableOfflineQueue: true,
  connectTimeout: 10000,

  // TLS for production
  tls: process.env.NODE_ENV === 'production' ? {} : undefined,
});

// Event handlers
redis.on('connect', () => console.log('Redis connecting...'));
redis.on('ready', () => console.log('Redis ready'));
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('close', () => console.log('Redis connection closed'));

// Cluster connection
export const cluster = new Redis.Cluster([
  { host: 'redis-node-1', port: 6379 },
  { host: 'redis-node-2', port: 6379 },
  { host: 'redis-node-3', port: 6379 },
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD,
  },
  scaleReads: 'slave',
  maxRedirections: 16,
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await redis.quit();
});

Caching Pattern

// patterns/cache.ts
import { redis } from './client';

interface CacheOptions {
  ttl?: number;  // seconds
  prefix?: string;
}

export class Cache {
  private prefix: string;
  private defaultTTL: number;

  constructor(options: CacheOptions = {}) {
    this.prefix = options.prefix || 'cache:';
    this.defaultTTL = options.ttl || 3600;
  }

  private key(key: string): string {
    return `${this.prefix}${key}`;
  }

  async get<T>(key: string): Promise<T | null> {
    const data = await redis.get(this.key(key));
    if (!data) return null;

    try {
      return JSON.parse(data) as T;
    } catch {
      return data as unknown as T;
    }
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const serialized = typeof value === 'string'
      ? value
      : JSON.stringify(value);

    await redis.setex(this.key(key), ttl || this.defaultTTL, serialized);
  }

  async getOrSet<T>(
    key: string,
    fetcher: () => Promise<T>,
    ttl?: number
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) return cached;

    const value = await fetcher();
    await this.set(key, value, ttl);
    return value;
  }

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

  async deletePattern(pattern: string): Promise<void> {
    const keys = await redis.keys(this.key(pattern));
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }

  // Cache with stale-while-revalidate
  async getStale<T>(
    key: string,
    fetcher: () => Promise<T>,
    options: { ttl: number; staleTTL: number }
  ): Promise<T> {
    const cacheKey = this.key(key);
    const staleKey = `${cacheKey}:stale`;

    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Check stale data
    const stale = await redis.get(staleKey);
    if (stale) {
      // Return stale, refresh in background
      this.refreshCache(key, fetcher, options).catch(console.error);
      return JSON.parse(stale);
    }

    return this.refreshCache(key, fetcher, options);
  }

  private async refreshCache<T>(
    key: string,
    fetcher: () => Promise<T>,
    options: { ttl: number; staleTTL: number }
  ): Promise<T> {
    const value = await fetcher();
    const serialized = JSON.stringify(value);

    const pipeline = redis.pipeline();
    pipeline.setex(this.key(key), options.ttl, serialized);
    pipeline.setex(`${this.key(key)}:stale`, options.staleTTL, serialized);
    await pipeline.exec();

    return value;
  }
}

// Usage
const cache = new Cache({ prefix: 'user:', ttl: 3600 });

async function getUser(id: string) {
  return cache.getOrSet(`profile:${id}`, async () => {
    return await db.users.findById(id);
  }, 1800);
}

Session Storage

// patterns/session.ts
import { redis } from './client';
import { nanoid } from 'nanoid';

interface Session {
  id: string;
  userId: string;
  data: Record<string, any>;
  createdAt: number;
  expiresAt: number;
}

export class SessionStore {
  private prefix = 'session:';
  private userPrefix = 'user:sessions:';
  private ttl = 86400 * 7; // 7 days

  private key(sessionId: string): string {
    return `${this.prefix}${sessionId}`;
  }

  async create(userId: string, data: Record<string, any> = {}): Promise<Session> {
    const session: Session = {
      id: nanoid(32),
      userId,
      data,
      createdAt: Date.now(),
      expiresAt: Date.now() + this.ttl * 1000,
    };

    const pipeline = redis.pipeline();

    // Store session
    pipeline.setex(this.key(session.id), this.ttl, JSON.stringify(session));

    // Track user's sessions
    pipeline.sadd(`${this.userPrefix}${userId}`, session.id);
    pipeline.expire(`${this.userPrefix}${userId}`, this.ttl);

    await pipeline.exec();

    return session;
  }

  async get(sessionId: string): Promise<Session | null> {
    const data = await redis.get(this.key(sessionId));
    if (!data) return null;

    const session = JSON.parse(data) as Session;

    // Check expiration
    if (session.expiresAt < Date.now()) {
      await this.destroy(sessionId);
      return null;
    }

    return session;
  }

  async update(sessionId: string, data: Record<string, any>): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) throw new Error('Session not found');

    session.data = { ...session.data, ...data };

    await redis.setex(
      this.key(sessionId),
      this.ttl,
      JSON.stringify(session)
    );
  }

  async refresh(sessionId: string): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) return;

    session.expiresAt = Date.now() + this.ttl * 1000;

    await redis.setex(
      this.key(sessionId),
      this.ttl,
      JSON.stringify(session)
    );
  }

  async destroy(sessionId: string): Promise<void> {
    const session = await this.get(sessionId);
    if (!session) return;

    const pipeline = redis.pipeline();
    pipeline.del(this.key(sessionId));
    pipeline.srem(`${this.userPrefix}${session.userId}`, sessionId);
    await pipeline.exec();
  }

  async destroyAllForUser(userId: string): Promise<void> {
    const sessionIds = await redis.smembers(`${this.userPrefix}${userId}`);

    if (sessionIds.length > 0) {
      const keys = sessionIds.map(id => this.key(id));
      await redis.del(...keys, `${this.userPrefix}${userId}`);
    }
  }
}

Rate Limiting

// patterns/rate-limiter.ts
import { redis } from './client';

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: number;
}

export class RateLimiter {
  // Fixed window rate limiting
  async fixedWindow(
    key: string,
    limit: number,
    windowSeconds: number
  ): Promise<RateLimitResult> {
    const redisKey = `ratelimit:fixed:${key}`;
    const now = Math.floor(Date.now() / 1000);
    const window = Math.floor(now / windowSeconds);
    const windowKey = `${redisKey}:${window}`;

    const count = await redis.incr(windowKey);

    if (count === 1) {
      await redis.expire(windowKey, windowSeconds);
    }

    return {
      allowed: count <= limit,
      remaining: Math.max(0, limit - count),
      resetAt: (window + 1) * windowSeconds * 1000,
    };
  }

  // Sliding window rate limiting
  async slidingWindow(
    key: string,
    limit: number,
    windowSeconds: number
  ): Promise<RateLimitResult> {
    const redisKey = `ratelimit:sliding:${key}`;
    const now = Date.now();
    const windowStart = now - windowSeconds * 1000;

    const pipeline = redis.pipeline();

    // Remove old entries
    pipeline.zremrangebyscore(redisKey, 0, windowStart);

    // Add current request
    pipeline.zadd(redisKey, now, `${now}:${Math.random()}`);

    // Count requests in window
    pipeline.zcard(redisKey);

    // Set expiration
    pipeline.expire(redisKey, windowSeconds);

    const results = await pipeline.exec();
    const count = results?.[2]?.[1] as number;

    return {
      allowed: count <= limit,
      remaining: Math.max(0, limit - count),
      resetAt: now + windowSeconds * 1000,
    };
  }

  // Token bucket rate limiting
  async tokenBucket(
    key: string,
    bucketSize: number,
    refillRate: number,  // tokens per second
    tokensNeeded: number = 1
  ): Promise<RateLimitResult> {
    const redisKey = `ratelimit:bucket:${key}`;
    const now = Date.now();

    // Lua script for atomic token bucket
    const script = `
      local key = KEYS[1]
      local bucket_size = tonumber(ARGV[1])
      local refill_rate = tonumber(ARGV[2])
      local tokens_needed = tonumber(ARGV[3])
      local now = tonumber(ARGV[4])

      local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
      local tokens = tonumber(bucket[1]) or bucket_size
      local last_refill = tonumber(bucket[2]) or now

      -- Calculate refill
      local elapsed = (now - last_refill) / 1000
      local refill = elapsed * refill_rate
      tokens = math.min(bucket_size, tokens + refill)

      -- Check if enough tokens
      local allowed = 0
      if tokens >= tokens_needed then
        tokens = tokens - tokens_needed
        allowed = 1
      end

      -- Save state
      redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
      redis.call('EXPIRE', key, math.ceil(bucket_size / refill_rate) + 1)

      return {allowed, tokens}
    `;

    const result = await redis.eval(
      script,
      1,
      redisKey,
      bucketSize,
      refillRate,
      tokensNeeded,
      now
    ) as [number, number];

    return {
      allowed: result[0] === 1,
      remaining: Math.floor(result[1]),
      resetAt: now + Math.ceil((tokensNeeded - result[1]) / refillRate) * 1000,
    };
  }
}

// Express middleware
export function rateLimitMiddleware(
  limiter: RateLimiter,
  options: { limit: number; window: number }
) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const key = req.ip || 'anonymous';
    const result = await limiter.slidingWindow(key, options.limit, options.window);

    res.setHeader('X-RateLimit-Limit', options.limit);
    res.setHeader('X-RateLimit-Remaining', result.remaining);
    res.setHeader('X-RateLimit-Reset', result.resetAt);

    if (!result.allowed) {
      return res.status(429).json({
        error: 'Too Many Requests',
        retryAfter: Math.ceil((result.resetAt - Date.now()) / 1000),
      });
    }

    next();
  };
}

Distributed Locks

// patterns/lock.ts
import { redis } from './client';
import { nanoid } from 'nanoid';

export class DistributedLock {
  private prefix = 'lock:';

  async acquire(
    resource: string,
    ttlMs: number = 10000
  ): Promise<string | null> {
    const lockKey = `${this.prefix}${resource}`;
    const lockValue = nanoid();
    const ttlSeconds = Math.ceil(ttlMs / 1000);

    const acquired = await redis.set(
      lockKey,
      lockValue,
      'EX',
      ttlSeconds,
      'NX'
    );

    return acquired === 'OK' ? lockValue : null;
  }

  async release(resource: string, lockValue: string): Promise<boolean> {
    const lockKey = `${this.prefix}${resource}`;

    // Lua script for atomic release
    const script = `
      if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
      else
        return 0
      end
    `;

    const result = await redis.eval(script, 1, lockKey, lockValue);
    return result === 1;
  }

  async extend(
    resource: string,
    lockValue: string,
    ttlMs: number
  ): Promise<boolean> {
    const lockKey = `${this.prefix}${resource}`;
    const ttlSeconds = Math.ceil(ttlMs / 1000);

    const script = `
      if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('EXPIRE', KEYS[1], ARGV[2])
      else
        return 0
      end
    `;

    const result = await redis.eval(script, 1, lockKey, lockValue, ttlSeconds);
    return result === 1;
  }

  async withLock<T>(
    resource: string,
    fn: () => Promise<T>,
    options: { ttl?: number; retries?: number; retryDelay?: number } = {}
  ): Promise<T> {
    const { ttl = 10000, retries = 3, retryDelay = 100 } = options;

    let lockValue: string | null = null;
    let attempts = 0;

    while (attempts < retries) {
      lockValue = await this.acquire(resource, ttl);
      if (lockValue) break;

      attempts++;
      await new Promise(resolve => setTimeout(resolve, retryDelay));
    }

    if (!lockValue) {
      throw new Error(`Failed to acquire lock for ${resource}`);
    }

    try {
      return await fn();
    } finally {
      await this.release(resource, lockValue);
    }
  }
}

// Usage
const lock = new DistributedLock();

async function processPayment(orderId: string) {
  return lock.withLock(`order:${orderId}`, async () => {
    // Critical section - only one process can execute
    const order = await db.orders.findById(orderId);
    if (order.status !== 'pending') {
      throw new Error('Order already processed');
    }

    await processStripePayment(order);
    await db.orders.update(orderId, { status: 'paid' });
  });
}

Pub/Sub Pattern

// patterns/pubsub.ts
import { Redis } from 'ioredis';

// Separate connections for pub/sub
const subscriber = new Redis(process.env.REDIS_URL!);
const publisher = new Redis(process.env.REDIS_URL!);

type MessageHandler<T> = (message: T, channel: string) => void | Promise<void>;

export class PubSub {
  private handlers: Map<string, Set<MessageHandler<any>>> = new Map();

  async subscribe<T>(channel: string, handler: MessageHandler<T>): Promise<void> {
    if (!this.handlers.has(channel)) {
      this.handlers.set(channel, new Set());
      await subscriber.subscribe(channel);
    }

    this.handlers.get(channel)!.add(handler);
  }

  async unsubscribe(channel: string, handler?: MessageHandler<any>): Promise<void> {
    const handlers = this.handlers.get(channel);
    if (!handlers) return;

    if (handler) {
      handlers.delete(handler);
      if (handlers.size === 0) {
        this.handlers.delete(channel);
        await subscriber.unsubscribe(channel);
      }
    } else {
      this.handlers.delete(channel);
      await subscriber.unsubscribe(channel);
    }
  }

  async publish<T>(channel: string, message: T): Promise<void> {
    await publisher.publish(channel, JSON.stringify(message));
  }

  async subscribePattern<T>(pattern: string, handler: MessageHandler<T>): Promise<void> {
    if (!this.handlers.has(pattern)) {
      this.handlers.set(pattern, new Set());
      await subscriber.psubscribe(pattern);
    }

    this.handlers.get(pattern)!.add(handler);
  }
}

// Initialize message handling
subscriber.on('message', (channel, message) => {
  const pubsub = new PubSub();
  const handlers = pubsub['handlers'].get(channel);
  if (!handlers) return;

  const parsed = JSON.parse(message);
  handlers.forEach(handler => handler(parsed, channel));
});

// Usage
const pubsub = new PubSub();

// Subscribe to user events
await pubsub.subscribe<{ userId: string; action: string }>('user:events', async (msg) => {
  console.log(`User ${msg.userId} performed ${msg.action}`);
});

// Publish event
await pubsub.publish('user:events', { userId: '123', action: 'login' });

Leaderboard Pattern

// patterns/leaderboard.ts
import { redis } from './client';

export class Leaderboard {
  private key: string;

  constructor(name: string) {
    this.key = `leaderboard:${name}`;
  }

  async addScore(member: string, score: number): Promise<void> {
    await redis.zadd(this.key, score, member);
  }

  async incrementScore(member: string, increment: number): Promise<number> {
    return redis.zincrby(this.key, increment, member);
  }

  async getTop(count: number): Promise<Array<{ member: string; score: number }>> {
    const results = await redis.zrevrange(this.key, 0, count - 1, 'WITHSCORES');

    const entries: Array<{ member: string; score: number }> = [];
    for (let i = 0; i < results.length; i += 2) {
      entries.push({
        member: results[i],
        score: parseFloat(results[i + 1]),
      });
    }

    return entries;
  }

  async getRank(member: string): Promise<number | null> {
    const rank = await redis.zrevrank(this.key, member);
    return rank !== null ? rank + 1 : null;
  }

  async getScore(member: string): Promise<number | null> {
    const score = await redis.zscore(this.key, member);
    return score !== null ? parseFloat(score) : null;
  }

  async getAroundMember(
    member: string,
    count: number
  ): Promise<Array<{ member: string; score: number; rank: number }>> {
    const rank = await redis.zrevrank(this.key, member);
    if (rank === null) return [];

    const start = Math.max(0, rank - Math.floor(count / 2));
    const results = await redis.zrevrange(this.key, start, start + count - 1, 'WITHSCORES');

    const entries: Array<{ member: string; score: number; rank: number }> = [];
    for (let i = 0; i < results.length; i += 2) {
      entries.push({
        member: results[i],
        score: parseFloat(results[i + 1]),
        rank: start + i / 2 + 1,
      });
    }

    return entries;
  }
}

Best Practices

  1. Connection pooling: Reuse connections
  2. Pipelining: Batch multiple commands
  3. TTL everywhere: Prevent memory leaks
  4. Key naming: Use consistent prefixes
  5. Lua scripts: Atomic operations
  6. Cluster ready: Design for horizontal scaling
  7. Error handling: Graceful degradation
  8. Memory management: Monitor and set maxmemory

Output Checklist

Every Redis implementation should include:

  • Connection with retry strategy
  • Proper key prefixing/namespacing
  • TTL on all keys
  • Error handling and fallbacks
  • Graceful shutdown
  • Pipelining for batch operations
  • Lua scripts for atomicity
  • Memory monitoring
  • Cluster-safe operations
  • Connection pooling

平台分布

Antigravity

32.48%
按下载量换算499

Claude Code

24.87%
按下载量换算382

Gemini CLI

16.89%
按下载量换算259

github-copilot

12.64%
按下载量换算194

windsurf

7.15%
按下载量换算110

Codex

3.2%
按下载量换算49

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

接触敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

继续浏览同类 Skills