Skip to main content

openclaw-saas-platform

Multi-tenant SaaS platform built on OpenClaw with auth, billing, workspace isolation, and AI agent execution gateway

インストールへ移動

ソース情報

リポジトリ
reason-machines/hermes-skills
ソースの最終更新活動
2026年8月4日 12:13
検出された SKILL.md の言語
英語
スター
5
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
openclaw-saas-platform
description
Multi-tenant SaaS platform built on OpenClaw with auth, billing, workspace isolation, and AI agent execution gateway
triggers
["how do I set up openclaw saas","implement openclaw multi-tenant platform","create openclaw saas billing system","add user authentication to openclaw","configure openclaw workspace isolation","integrate openclaw agent gateway","deploy openclaw saas platform","test openclaw saas features"]
# OpenClaw SaaS Platform > Skill by [ara.so](https://ara.so) — Hermes Skills collection. OpenClaw SaaS transforms the open-source OpenClaw AI Agent into a multi-tenant SaaS platform with account management, credit-based billing, subscription orders, workspace isolation, and real-time agent execution. Built with FastAPI backend, React 19 frontend, and dual-container Docker architecture. ## Core Architecture **Dual-Container Design:** - **Backend Container**: FastAPI app handling auth, billing, orders, API gateway - **OpenClaw Gateway Container**: Isolated AI agent execution engine - **Shared Volume**: `/opt/workspaces/{agent_id}/` for file isolation - **Dependencies**: MySQL 8.4, Redis 7 **Key Systems:** - Account system with SMS verification + JWT - Credit-based billing with transaction ledger - Subscription orders with state machine - Multi-user workspace isolation - Real-time Socket.IO communication - Rate limiting and blacklist protection ## Installation ### Local Development Setup ```bash # 1. Clone and setup dependencies git clone https://github.com/xingzhicn/openclaw-saas.git cd openclaw-saas # 2. Start MySQL + Redis via Docker docker-compose -f docker-compose.local.yml up -d # 3. Backend setup python3.12 -m venv venv source venv/bin/activate pip install -r requirements.txt # 4. Configure environment cat > .env <<EOF DATABASE_URL=mysql+aiomysql://openclaw:openclaw_dev_pwd@localhost:3307/openclaw_saas REDIS_URL=redis://localhost:6380/0 ENCRYPTION_KEY=$(openssl rand -hex 32) JWT_SECRET_KEY=$(openssl rand -hex 32) OPENCLAW_TOKEN=$(openssl rand -hex 16) WORKSPACE_BASE=/opt/workspaces ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} OPENAI_API_KEY=${OPENAI_API_KEY} DEBUG=true EOF # 5. Initialize database python -c "from backend.app.database import init_db; import asyncio; asyncio.run(init_db())" # 6. Start backend uvicorn backend.app.main:app --reload --port 8000 # 7. Start frontend (new terminal) cd frontend npm install npm run dev # http://localhost:5173 ``` ### Production Deployment ```bash # 1. Build Docker images ./scripts/pack-backend.sh ./scripts/pack-openclaw.sh # 2. Transfer to server scp openclaw-*.tar.gz deploy-*.sh root@your-server:/opt/ # 3. Deploy on server ssh root@your-server cd /opt ./deploy-all.sh ``` ## Core API Patterns ### Authentication Flow ```python # backend/app/api/auth.py from fastapi import APIRouter, Depends, HTTPException from backend.app.middleware.auth import get_current_user from backend.app.models.user import User from backend.app.utils.sms import send_sms_code from backend.app.utils.jwt import create_access_token router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) @router.post("/send-code") async def send_verification_code(request: dict, db: AsyncSession = Depends(get_db)): """Send SMS verification code with rate limiting""" phone = request["phone"] # Check rate limit: 1 request per 60s cache_key = f"sms:ratelimit:{phone}" if await redis_client.exists(cache_key): raise HTTPException(status_code=429, detail="请等待60秒后再试") # Check daily limit: 3 requests per day daily_key = f"sms:daily:{phone}" daily_count = await redis_client.get(daily_key) if daily_count and int(daily_count) >= 3: raise HTTPException(status_code=429, detail="今日发送次数已达上限") # Generate and send code code = generate_sms_code() await send_sms_code(phone, code) # Cache code for 5 minutes await redis_client.setex(f"sms:code:{phone}", 300, code) await redis_client.setex(cache_key, 60, "1") await redis_client.incr(daily_key) await redis_client.expire(daily_key, 86400) return {"message": "验证码已发送"} @router.post("/login") async def login(request: dict, db: AsyncSession = Depends(get_db)): """Login with phone and SMS code""" phone, code = request["phone"], request["code"] # Verify code cached_code = await redis_client.get(f"sms:code:{phone}") if not cached_code or cached_code != code: raise HTTPException(status_code=400, detail="验证码错误或已过期") # Get or create user result = await db.execute(select(User).where(User.phone == phone)) user = result.scalar_one_or_none() if not user: user = User(phone=phone, credits=1000) # 1000 initial credits db.add(user) await db.commit() await db.refresh(user) # Generate JWT token = create_access_token({"sub": str(user.id)}) return { "access_token": token, "user": { "id": user.id, "phone": user.phone, "credits": user.credits } } ``` ### Credit Billing System ```python # backend/app/api/credits.py from backend.app.models.credit_transaction import CreditTransaction, TransactionType from backend.app.utils.credits import freeze_credits, consume_credits, refund_credits @router.post("/freeze") async def freeze_user_credits( request: dict, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): """Freeze credits before agent execution""" amount = request["amount"] request_id = request["request_id"] # Check sufficient credits if current_user.credits < amount: raise HTTPException(status_code=400, detail="积分不足") # Freeze in Redis await redis_client.hset( f"freeze:{request_id}", mapping={"user_id": current_user.id, "amount": amount} ) await redis_client.expire(f"freeze:{request_id}", 3600) # Deduct from user balance current_user.credits -= amount await db.commit() # Create freeze transaction transaction = CreditTransaction( user_id=current_user.id, amount=amount, type=TransactionType.FREEZE, description=f"冻结积分: {request_id}", request_id=request_id ) db.add(transaction) await db.commit() return {"frozen_amount": amount, "remaining_credits": current_user.credits} @router.post("/consume") async def consume_frozen_credits( request: dict, db: AsyncSession = Depends(get_db) ): """Consume frozen credits after agent execution""" request_id = request["request_id"] actual_amount = request["actual_amount"] # Get freeze info freeze_data = await redis_client.hgetall(f"freeze:{request_id}") if not freeze_data: raise HTTPException(status_code=404, detail="冻结记录不存在") frozen_amount = int(freeze_data["amount"]) user_id = int(freeze_data["user_id"]) # Consume transaction transaction = CreditTransaction( user_id=user_id, amount=actual_amount, type=TransactionType.CONSUME, description=f"消耗积分: {request_id}", request_id=request_id ) db.add(transaction) # Refund excess if any if actual_amount < frozen_amount: refund_amount = frozen_amount - actual_amount result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one() user.credits += refund_amount refund_tx = CreditTransaction( user_id=user_id, amount=refund_amount, type=TransactionType.REFUND, description=f"退回冻结余额: {request_id}", request_id=request_id ) db.add(refund_tx) await redis_client.delete(f"freeze:{request_id}") await db.commit() return {"consumed": actual_amount, "refunded": frozen_amount - actual_amount} ``` ### Workspace Isolation ```python # backend/app/utils/workspace.py import os import shutil from pathlib import Path class WorkspaceManager: def __init__(self, base_path: str = "/opt/workspaces"): self.base_path = Path(base_path) def get_workspace_path(self, agent_id: str) -> Path: """Get isolated workspace path for agent""" workspace = self.base_path / str(agent_id) workspace.mkdir(parents=True, exist_ok=True) return workspace def create_workspace(self, agent_id: str) -> Path: """Create new workspace with initial structure""" workspace = self.get_workspace_path(agent_id) # Create subdirectories (workspace / "input").mkdir(exist_ok=True) (workspace / "output").mkdir(exist_ok=True) (workspace / "logs").mkdir(exist_ok=True) return workspace def cleanup_workspace(self, agent_id: str): """Remove workspace files (keep for 7 days in production)""" workspace = self.get_workspace_path(agent_id) if workspace.exists(): shutil.rmtree(workspace) def write_input_file(self, agent_id: str, filename: str, content: str): """Write file to workspace input directory""" workspace = self.get_workspace_path(agent_id) input_path = workspace / "input" / filename input_path.write_text(content) return input_path def read_output_file(self, agent_id: str, filename: str) -> str: """Read file from workspace output directory""" workspace = self.get_workspace_path(agent_id) output_path = workspace / "output" / filename if not output_path.exists(): raise FileNotFoundError(f"{filename} not found in output") return output_path.read_text() # Usage in API @router.post("/agents/execute") async def execute_agent( request: dict, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): agent_id = str(uuid.uuid4()) workspace_manager = WorkspaceManager() # Create isolated workspace workspace = workspace_manager.create_workspace(agent_id) # Write input files workspace_manager.write_input_file(agent_id, "task.txt", request["task"]) # Execute agent in OpenClaw Gateway container # (shares /opt/workspaces via Docker volume) result = await execute_openclaw_agent(agent_id, workspace) # Read output output = workspace_manager.read_output_file(agent_id, "result.txt") return {"agent_id": agent_id, "output": output} ``` ### Agent Execution Gateway ```python # backend/app/tasks/agent_executor.py import httpx from backend.app.config import settings async def execute_openclaw_agent(agent_id: str, workspace: Path) -> dict: """Execute agent in isolated OpenClaw Gateway container""" # OpenClaw Gateway runs in separate container gateway_url = settings.OPENCLAW_GATEWAY_URL async with httpx.AsyncClient() as client: response = await client.post( f"{gateway_url}/execute", json={ "agent_id": agent_id, "workspace": str(workspace), "model": "claude-3-5-sonnet-20241022", "api_key": settings.ANTHROPIC_API_KEY }, headers={"Authorization": f"Bearer {settings.OPENCLAW_TOKEN}"}, timeout=300.0 ) if response.status_code != 200: raise Exception(f"Agent execution failed: {response.text}") return response.json() # Real-time status updates via Socket.IO from socketio import AsyncServer sio = AsyncServer(async_mode='asgi', cors_allowed_origins='*') async def stream_agent_status(agent_id: str, user_id: int): """Stream agent execution status to frontend""" await sio.emit('agent_status', { 'agent_id': agent_id, 'status': 'running' }, room=f"user_{user_id}") # ... agent execution ... await sio.emit('agent_status', { 'agent_id': agent_id, 'status': 'completed', 'result': result }, room=f"user_{user_id}") ``` ## Testing Patterns OpenClaw SaaS uses "real" testing with database/Redis operations and four-dimensional verification: ```python # tests/test_credits.py import pytest from tests.helpers.verifier import verify_db, verify_cache, verify_workspace, verify_response def test_freeze_consume_workflow(client, auth_headers, test_user, db, redis_client): """Test complete freeze -> consume -> refund workflow""" request_id = "req_freeze_001" # 1. Freeze credits response = client.post( "/api/v1/credits/freeze", json={"amount": 500, "request_id": request_id}, headers=auth_headers )
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る