Fastapi Storage Module Skill logo

Fastapi Storage Module Skill

Community
jiushiwon
fastapi-storage-module-skill

FastAPI 静态资源库模块技能。面向已有 FastAPI 项目,提供小文件直接上传、大文件切割上传、文件压缩、下载、预览、附件管理等能力。触发词:"静态资源模块"、"Python 文件上传"、"FastAPI 文件上传"、"大文件上传"、"文件压缩"、"storage module"、"资源管理"。

Overview

Publisherjiushiwon
Repositorywg-skills
Skill namefastapi-storage-module-skill
Stars
123
Forks
38
Bundled files
1
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by jiushiwon on GitHub. Read the source before you install it.

Installation

Install the Fastapi Storage Module Skill AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/jiushiwon/wg-skills/tree/main/vibeCoding/backend/python/fastapi-module/fastapi-storage-module-skill
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/jiushiwon/wg-skills.git /tmp/wg-skills
mkdir -p .claude/skills
cp -r /tmp/wg-skills/vibeCoding/backend/python/fastapi-module/fastapi-storage-module-skill .claude/skills/fastapi-storage-module-skill
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Fastapi Storage Module Skill in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Fastapi Storage Module Skill on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Fastapi Storage Module Skill is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

FastAPI Storage Module Skill

面向已有 FastAPI 项目的开发者,快速集成静态资源管理能力。

定位

  • 目标:在已有 fastapi-init-skill 骨架上,添加可运行的静态资源管理模块。
  • 不替代:不重复生成 fastapi-init-skill 已经提供的统一响应、JWT、SQLModel 等基础设施。
  • 输出:模型、仓储、服务、路由、数据库迁移、接口契约、接入指南。

骨架依赖

本模块是 fastapi-init-skill 的子模块,必须在骨架基础上使用。

使用前必须满足:

  1. ✅ 已安装 fastapi-init-skill(项目骨架)
  2. ✅ 骨架包含:JWT、统一响应、SQLModel、分页、目录结构

检测逻辑:

  1. 读取用户项目根目录 SKILL.md 或 README.md
  2. 检测是否包含 fastapi-init-skill 相关内容
  3. 如未检测到骨架,提示:"本模块需要先安装 fastapi-init-skill 骨架"

用户问题(最多 3 个)

1. 现有项目的包名是什么?(默认从骨架推断,如 app)
2. 表前缀是什么?(默认 wg)
3. 默认存储方式是什么?(local / aliyun / tencent / minio,默认 local)

核心能力清单

#能力说明
1小文件上传单文件/多文件直接上传(< 10MB)
2大文件切割上传分片上传 + 断点续传(≥ 10MB)
3文件压缩可选开启,支持 Pillow 压缩图片
4文件下载流式下载、断点续传下载
5文件预览图片/视频/文档在线预览
6存储策略本地存储 / 阿里云 OSS / 腾讯云 COS / MinIO
7附件管理附件 CRUD、分类、标签
8图片处理缩略图、水印、格式转换

配置结构

python
# config.py
from pydantic import BaseSettings
from typing import List, Optional

class ChunkConfig(BaseSettings):
    enabled: bool = True
    size: int = 5 * 1024 * 1024  # 5MB 每片
    threshold: int = 10 * 1024 * 1024  # 10MB 触发分片上传

class CompressConfig(BaseSettings):
    enabled: bool = False
    types: List[str] = ["jpg", "jpeg", "png"]
    quality: float = 0.8
    max_size: int = 1024 * 1024  # 1MB 以上才压缩

class LocalConfig(BaseSettings):
    path: str = "./uploads"
    domain: str = "http://localhost:8000"

class AliyunConfig(BaseSettings):
    access_key: str = ""
    secret_key: str = ""
    bucket: str = ""
    endpoint: str = "oss-cn-hangzhou.aliyuncs.com"

class TencentConfig(BaseSettings):
    secret_id: str = ""
    secret_key: str = ""
    bucket: str = ""
    region: str = "ap-guangzhou"

class MinioConfig(BaseSettings):
    endpoint: str = "http://localhost:9000"
    access_key: str = "minioadmin"
    secret_key: str = "minioadmin"
    bucket: str = "uploads"

class StorageConfig(BaseSettings):
    type: str = "local"  # local / aliyun / tencent / minio
    max_size: int = 100 * 1024 * 1024  # 100MB
    allowed_types: List[str] = [
        "jpg", "jpeg", "png", "gif", "pdf", 
        "doc", "docx", "xls", "xlsx", "zip", "mp4", "mp3"
    ]
    chunk: ChunkConfig = ChunkConfig()
    compress: CompressConfig = CompressConfig()
    local: LocalConfig = LocalConfig()
    aliyun: AliyunConfig = AliyunConfig()
    tencent: TencentConfig = TencentConfig()
    minio: MinioConfig = MinioConfig()

storage_config = StorageConfig()

模块结构

src/storage/
├── __init__.py
├── config.py                   # 配置
├── constants.py                # 常量
├── exceptions.py               # 异常
├── models.py                   # SQLModel 模型
├── schemas.py                  # Pydantic schemas
├── routers/
│   ├── __init__.py
│   ├── upload.py               # 上传接口
│   ├── download.py             # 下载接口
│   └── attachment.py           # 附件管理接口
└── services/
    ├── __init__.py
    ├── storage_service.py      # 存储策略接口
    ├── local_storage.py        # 本地存储
    ├── oss_storage.py          # 阿里云 OSS
    ├── cos_storage.py          # 腾讯云 COS
    ├── minio_storage.py        # MinIO
    ├── upload_service.py       # 上传服务
    ├── download_service.py     # 下载服务
    ├── attachment_service.py   # 附件服务
    └── image_service.py        # 图片处理服务

alembic/versions/storage_module.py  # 迁移文件

api-contract-storage.md             # 接口契约
docs/storage-module-guide.md        # 接入指南

核心实现

存储策略接口

python
# services/storage_service.py
from abc import ABC, abstractmethod
from typing import BinaryIO, Optional

class StorageService(ABC):
    """存储策略抽象接口"""
    
    @abstractmethod
    async def upload(self, file: BinaryIO, key: str, content_type: str) -> str:
        """上传文件
        
        Args:
            file: 文件流
            key: 存储 key
            content_type: 内容类型
            
        Returns:
            访问 URL
        """
        pass
    
    @abstractmethod
    async def delete(self, key: str) -> None:
        """删除文件"""
        pass
    
    @abstractmethod
    async def get_url(self, key: str) -> str:
        """获取文件 URL"""
        pass
    
    @abstractmethod
    async def download(self, key: str) -> BinaryIO:
        """获取文件流"""
        pass

本地存储实现

python
# services/local_storage.py
import os
import aiofiles
from pathlib import Path
from datetime import datetime
from .storage_service import StorageService
from ..config import storage_config

class LocalStorageService(StorageService):
    """本地存储实现"""
    
    def __init__(self):
        self.base_path = Path(storage_config.local.path)
        self.domain = storage_config.local.domain
        self.base_path.mkdir(parents=True, exist_ok=True)
    
    async def upload(self, file: BinaryIO, key: str, content_type: str) -> str:
        """上传文件到本地"""
        file_path = self.base_path / key
        file_path.parent.mkdir(parents=True, exist_ok=True)
        
        async with aiofiles.open(file_path, 'wb') as f:
            content = await file.read()
            await f.write(content)
        
        return f"{self.domain}/{key}"
    
    async def delete(self, key: str) -> None:
        """删除本地文件"""
        file_path = self.base_path / key
        if file_path.exists():
            file_path.unlink()
    
    async def get_url(self, key: str) -> str:
        """获取文件 URL"""
        return f"{self.domain}/{key}"
    
    async def download(self, key: str) -> BinaryIO:
        """获取文件流"""
        file_path = self.base_path / key
        if not file_path.exists():
            raise FileNotFoundError(f"文件不存在: {key}")
        return open(file_path, 'rb')

上传服务(核心)

python
# services/upload_service.py
import uuid
import math
from datetime import datetime
from typing import List, Optional, BinaryIO
from fastapi import UploadFile
from sqlmodel import Session, select

from ..config import storage_config
from ..models import Attachment, ChunkInfo
from ..schemas import UploadResult, ChunkUploadInit
from ..exceptions import StorageException
from .storage_service import StorageService
from .local_storage import LocalStorageService
from .image_service import ImageService

class UploadService:
    """上传服务"""
    
    def __init__(self, session: Session):
        self.session = session
        self.storage: StorageService = self._get_storage_service()
        self.image_service = ImageService()
    
    def _get_storage_service(self) -> StorageService:
        """获取存储服务实例"""
        storage_type = storage_config.type
        if storage_type == "local":
            return LocalStorageService()
        # elif storage_type == "aliyun":
        #     return OssStorageService()
        # elif storage_type == "tencent":
        #     return CosStorageService()
        # elif storage_type == "minio":
        #     return MinioStorageService()
        else:
            raise StorageException(f"不支持的存储类型: {storage_type}")
    
    async def upload(self, file: UploadFile, category: str = "default") -> UploadResult:
        """上传文件(自动判断小文件/大文件)"""
        # 验证文件
        await self._validate_file(file)
        
        # 判断是否需要分片上传
        if self._need_chunk_upload(file.size):
            return await self._chunk_upload(file, category)
        
        # 小文件直接上传
        return await self._direct_upload(file, category)
    
    async def _direct_upload(self, file: UploadFile, category: str) -> UploadResult:
        """小文件直接上传"""
        key = self._generate_key(file.filename)
        content_type = file.content_type or "application/octet-stream"
        
        # 是否需要压缩
        content = await file.read()
        if self._need_compress(file.filename, len(content)):
            content = await self.image_service.compress(
                content, 
                storage_config.compress.quality
            )
        
        # 上传
        from io import BytesIO
        url = await self.storage.upload(BytesIO(content), key, content_type)
        
        # 保存附件记录
        attachment = Attachment(
            filename=file.filename,
            file_path=key,
            file_url=url,
            file_size=len(content),
            content_type=content_type,
            file_ext=self._get_extension(file.filename),
            category=category,
            storage_type=storage_config.type,
            is_compressed=self._need_compress(file.filename, file.size)
        )
        self.session.add(attachment)
        self.session.commit()
        self.session.refresh(attachment)
        
        return UploadResult(
            id=attachment.id,
            url=url,
            filename=file.filename,
            size=len(content),
            content_type=content_type
        )
    
    async def _chunk_upload(self, file: UploadFile, category: str) -> UploadResult:
        """大文件分片上传"""
        # 1. 初始化分片上传
        upload_id = str(uuid.uuid4())
        chunk_size = storage_config.chunk.size
        total_chunks = math.ceil(file.size / chunk_size)
        
        # 2. 保存分片信息
        chunk_info = ChunkInfo(
            upload_id=upload_id,
            filename=file.filename,
            file_size=file.size,
            chunk_size=chunk_size,
            total_chunks=total_chunks,
            status=0  # 上传中
        )
        self.session.add(chunk_info)
        self.session.commit()
        
        # 3. 分片上传
        for chunk_index in range(total_chunks):
            content = await file.read(chunk_size)
            chunk_key = f"chunks/{upload_id}/{chunk_index}"
            from io import BytesIO
            await self.storage.upload(
                BytesIO(content),
                chunk_key,
                "application/octet-stream"
            )
        
        # 4. 合并分片
        key = self._generate_key(file.filename)
        await self._merge_chunks(upload_id, key, total_chunks)
        
        # 5. 更新状态
        chunk_info.status = 1  # 已完成
        self.session.commit()
        
        # 6. 保存附件记录
        url = await self.storage.get_url(key)
        attachment = Attachment(
            filename=file.filename,
            file_path=key,
            file_url=url,
            file_size=file.size,
            content_type=file.content_type,
            file_ext=self._get_extension(file.filename),
            category=category,
            storage_type=storage_config.type,
            is_compressed=False
        )
        self.session.add(attachment)
        self.session.commit()
        self.session.refresh(attachment)
        
        return UploadResult(
            id=attachment.id,
            url=url,
            filename=file.filename,
            size=file.size,
            content_type=file.content_type
        )
    
    async def get_uploaded_chunks(self, upload_id: str) -> List[int]:
        """获取已上传分片(断点续传)"""
        chunk_info = self.session.exec(
            select(ChunkInfo).where(ChunkInfo.upload_id == upload_id)
        ).first()
        
        if not chunk_info:
            raise StorageException("上传任务不存在")
        
        uploaded = []
        for i in range(chunk_info.total_chunks):
            chunk_key = f"chunks/{upload_id}/{i}"
            try:
                await self.storage.download(chunk_key)
                uploaded.append(i)
            except Exception:
                pass
        
        return uploaded
    
    async def upload_chunk(self, upload_id: str, chunk_index: int, file: UploadFile):
        """上传单个分片(断点续传)"""
        chunk_info = self.session.exec(
            select(ChunkInfo).where(ChunkInfo.upload_id == upload_id)
        ).first()
        
        if not chunk_info:
            raise StorageException("上传任务不存在")
        
        chunk_key = f"chunks/{upload_id}/{chunk_index}"
        content = await file.read()
        from io import BytesIO
        await self.storage.upload(BytesIO(content), chunk_key, "application/octet-stream")
    
    def _need_chunk_upload(self, file_size: int) -> bool:
        """判断是否需要分片上传"""
        return (storage_config.chunk.enabled and 
                file_size >= storage_config.chunk.threshold)
    
    def _need_compress(self, filename: str, file_size: int) -> bool:
        """判断是否需要压缩"""
        if not storage_config.compress.enabled:
            return False
        if file_size < storage_config.compress.max_size:
            return False
        ext = self._get_extension(filename)
        return ext.lower() in storage_config.compress.types
    
    def _generate_key(self, filename: str) -> str:
        """生成存储 key"""
        ext = self._get_extension(filename)
        date_path = datetime.now().strftime("%Y/%m/%d")
        return f"uploads/{date_path}/{uuid.uuid4()}.{ext}"
    
    def _get_extension(self, filename: str) -> str:
        """获取文件扩展名"""
        return filename.rsplit(".", 1)[-1] if "." in filename else ""
    
    async def _validate_file(self, file: UploadFile):
        """验证文件"""
        if not file.filename:
            raise StorageException("文件不能为空")
        if file.size > storage_config.max_size:
            raise StorageException("文件大小超过限制")
        ext = self._get_extension(file.filename)
        if ext.lower() not in storage_config.allowed_types:
            raise StorageException("不支持的文件类型")

上传路由

python
# routers/upload.py
from typing import List, Optional
from fastapi import APIRouter, UploadFile, File, Form, Depends
from sqlmodel import Session

from ..schemas import UploadResult, ChunkUploadInit
from ..services.upload_service import UploadService
from ...core.deps import get_session

router = APIRouter(prefix="/api/storage", tags=["存储管理"])

@router.post("/upload", response_model=UploadResult)
async def upload_file(
    file: UploadFile = File(...),
    category: str = Form(default="default"),
    session: Session = Depends(get_session)
):
    """上传文件(自动判断小文件/大文件)"""
    service = UploadService(session)
    return await service.upload(file, category)

@router.post("/upload/chunk/init", response_model=ChunkUploadInit)
async def init_chunk_upload(
    filename: str = Form(...),
    file_size: int = Form(...),
    session: Session = Depends(get_session)
):
    """初始化分片上传"""
    service = UploadService(session)
    return await service.init_chunk_upload(filename, file_size)

@router.post("/upload/chunk/{upload_id}/{chunk_index}")
async def upload_chunk(
    upload_id: str,
    chunk_index: int,
    file: UploadFile = File(...),
    session: Session = Depends(get_session)
):
    """上传分片"""
    service = UploadService(session)
    await service.upload_chunk(upload_id, chunk_index, file)
    return {"message": "分片上传成功"}

@router.post("/upload/chunk/{upload_id}/merge", response_model=UploadResult)
async def merge_chunks(
    upload_id: str,
    filename: str = Form(...),
    category: str = Form(default="default"),
    session: Session = Depends(get_session)
):
    """合并分片"""
    service = UploadService(session)
    return await service.merge_chunks(upload_id, filename, category)

@router.get("/upload/chunk/{upload_id}/chunks", response_model=List[int])
async def get_uploaded_chunks(
    upload_id: str,
    session: Session = Depends(get_session)
):
    """获取已上传分片(断点续传)"""
    service = UploadService(session)
    return await service.get_uploaded_chunks(upload_id)

下载路由

python
# routers/download.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Session

from ..services.download_service import DownloadService
from ...core.deps import get_session

router = APIRouter(prefix="/api/storage", tags=["存储管理"])

@router.get("/download/{attachment_id}")
async def download_file(
    attachment_id: int,
    session: Session = Depends(get_session)
):
    """下载文件"""
    service = DownloadService(session)
    file_stream, filename, content_type = await service.download(attachment_id)
    
    return StreamingResponse(
        file_stream,
        media_type=content_type,
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"'
        }
    )

@router.get("/preview/{attachment_id}")
async def preview_file(
    attachment_id: int,
    session: Session = Depends(get_session)
):
    """预览文件"""
    service = DownloadService(session)
    file_stream, filename, content_type = await service.preview(attachment_id)
    
    return StreamingResponse(
        file_stream,
        media_type=content_type,
        headers={
            "Content-Disposition": f'inline; filename="{filename}"'
        }
    )

数据库模型

python
# models.py
from datetime import datetime
from typing import Optional
from sqlmodel import SQLModel, Field

class Attachment(SQLModel, table=True):
    __tablename__ = "{prefix}_attachment"
    
    id: Optional[int] = Field(default=None, primary_key=True)
    filename: str = Field(max_length=255, description="原始文件名")
    file_path: str = Field(max_length=500, description="存储路径")
    file_url: str = Field(max_length=500, description="访问 URL")
    file_size: int = Field(description="文件大小(字节)")
    content_type: Optional[str] = Field(max_length=100, description="内容类型")
    file_ext: Optional[str] = Field(max_length=20, description="文件扩展名")
    category: str = Field(default="default", max_length=50, description="分类")
    storage_type: str = Field(max_length=20, description="存储类型")
    uploader_id: Optional[int] = Field(description="上传者 ID")
    is_compressed: bool = Field(default=False, description="是否已压缩")
    created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
    deleted_at: Optional[datetime] = Field(default=None, description="软删除时间")

class ChunkInfo(SQLModel, table=True):
    __tablename__ = "{prefix}_chunk_info"
    
    id: Optional[int] = Field(default=None, primary_key=True)
    upload_id: str = Field(max_length=64, unique=True, description="上传任务 ID")
    filename: str = Field(max_length=255, description="文件名")
    file_size: int = Field(description="文件总大小")
    chunk_size: int = Field(description="分片大小")
    total_chunks: int = Field(description="总分片数")
    status: int = Field(default=0, description="状态 0-上传中 1-已完成 2-已取消")
    created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
    updated_at: datetime = Field(default_factory=datetime.now, description="更新时间")

接口契约要点

方法路径说明
POST/api/storage/upload上传文件(自动判断)
POST/api/storage/upload/chunk/init初始化分片上传
POST/api/storage/upload/chunk/{upload_id}/{index}上传分片
POST/api/storage/upload/chunk/{upload_id}/merge合并分片
GET/api/storage/upload/chunk/{upload_id}/chunks获取已上传分片
GET/api/storage/download/{id}下载文件
GET/api/storage/preview/{id}预览文件
GET/api/storage/attachment/list附件列表
GET/api/storage/attachment/{id}附件详情
DELETE/api/storage/attachment/{id}删除附件

强制交付物

文档位置说明
接口契约api-contract-storage.md全量接口
接入指南docs/storage-module-guide.md表结构、配置、集成步骤

红线

  1. 不重复生成 FastAPI 基础骨架。
  2. 表名统一 {prefix}_attachment、{prefix}_chunk_info。
  3. 所有删除为软删除(deleted_at)。
  4. 分片上传必须支持断点续传。
  5. 压缩功能必须可配置开关。
  6. 所有注释、文档用中文。
  7. 与 springboot-storage-module-skill 保持 API 字段完全一致。

【考拉搞AI】,带你全面进入 VibeCoding 的世界~

触发关键词

静态资源模块、Python 文件上传、FastAPI 文件上传、大文件上传、
文件压缩、storage module、资源管理、分片上传、断点续传

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Fastapi Storage Module Skill AI skill do?

FastAPI 静态资源库模块技能。面向已有 FastAPI 项目,提供小文件直接上传、大文件切割上传、文件压缩、下载、预览、附件管理等能力。触发词:"静态资源模块"、"Python 文件上传"、"FastAPI 文件上传"、"大文件上传"、"文件压缩"、"storage module"、"资源管理"。

Why use Fastapi Storage Module Skill on TypingMind?

Because you install it once and use it with any model. Fastapi Storage Module Skill is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Fastapi Storage Module Skill in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/jiushiwon/wg-skills/tree/main/vibeCoding/backend/python/fastapi-module/fastapi-storage-module-skill. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Fastapi Storage Module Skill?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Fastapi Storage Module Skill?

As many as you like. As long as a model supports skills, you can use Fastapi Storage Module Skill with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Fastapi Storage Module Skill AI skill free?

Yes. It is published on GitHub by jiushiwon under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇