diff --git a/.env.example b/.env.example index 93e4a1e..e519005 100644 --- a/.env.example +++ b/.env.example @@ -3,21 +3,41 @@ # Скопируйте этот файл в .env и заполните значения # ───────────────────────────────────────────── -# Ollama API (обязательно для работы чата и списка моделей) +# ── LLM Provider ─────────────────────────────── +# Option 1: Ollama Cloud (default) +LLM_BASE_URL=https://ollama.com/v1 +LLM_API_KEY=your_api_key_here + +# Option 2: Direct Ollama (legacy aliases) OLLAMA_BASE_URL=https://ollama.com/v1 -OLLAMA_API_KEY=your_ollama_api_key_here +OLLAMA_API_KEY= -# База данных MySQL/TiDB -DATABASE_URL=mysql://goclaw:password@localhost:3306/goclaw +# Option 3: OpenAI-compatible +# LLM_BASE_URL=https://api.openai.com/v1 +# LLM_API_KEY=sk-... -# JWT Secret — случайная строка для подписи сессионных токенов -# Сгенерировать: openssl rand -hex 32 -JWT_SECRET=change_me_to_random_secret +# Default model for orchestrator (when DB config unavailable) +DEFAULT_MODEL=qwen2.5:7b -# Telegram Bot (опционально) -TELEGRAM_BOT_TOKEN= -TELEGRAM_WEBHOOK_URL= +# ── Database ─────────────────────────────────── +# MySQL/TiDB connection string +DATABASE_URL=mysql://goclaw:goClawPass123@localhost:3306/goclaw -# GoClaw Gateway (опционально) +# ── Authentication ───────────────────────────── +# JWT Secret — generate: openssl rand -hex 32 +JWT_SECRET=change_me_to_a_random_64_char_string + +# ── GoClaw Gateway ───────────────────────────── +# URL of the Go Gateway (blank = fallback to Node.js orchestrator) GATEWAY_URL=http://localhost:18789 -GATEWAY_API_KEY= + +# ── Manus OAuth (optional) ───────────────────── +VITE_APP_ID= +OAUTH_SERVER_URL= +VITE_OAUTH_PORTAL_URL= + +# ── Manus Built-in APIs (optional) ───────────── +BUILT_IN_FORGE_API_URL= +BUILT_IN_FORGE_API_KEY= +VITE_FRONTEND_FORGE_API_KEY= +VITE_FRONTEND_FORGE_API_URL= \ No newline at end of file diff --git a/docker/Dockerfile.control-center b/docker/Dockerfile.control-center index 713df7a..294bf51 100644 --- a/docker/Dockerfile.control-center +++ b/docker/Dockerfile.control-center @@ -11,14 +11,14 @@ COPY package.json pnpm-lock.yaml ./ COPY patches/ ./patches/ RUN pnpm install --frozen-lockfile -# Copy source code (exclude gateway/ and docker/) +# Copy all source code needed for build COPY client/ ./client/ COPY server/ ./server/ COPY shared/ ./shared/ COPY drizzle/ ./drizzle/ -COPY drizzle.config.ts tsconfig.json vite.config.ts ./ +COPY drizzle.config.ts tsconfig.json tsconfig.node.json vite.config.ts vitest.config.ts ./ -# Build frontend and backend +# Build frontend (Vite) and backend (esbuild) RUN pnpm build # ── Runtime Stage ───────────────────────────────────────────────────────────── @@ -28,12 +28,10 @@ FROM node:22-alpine RUN apk add --no-cache \ wget \ curl \ + bash \ + netcat-openbsd \ && rm -rf /var/cache/apk/* -# Create non-root user -RUN addgroup -g 1001 goclaw && \ - adduser -u 1001 -G goclaw -s /bin/sh -D goclaw - WORKDIR /app # Install pnpm @@ -42,22 +40,24 @@ RUN npm install -g pnpm@latest # Copy package files and install production deps only COPY package.json pnpm-lock.yaml ./ COPY patches/ ./patches/ -# Install all deps (vite is needed at runtime for SSR/proxy, drizzle-kit for migrations) +# Install all deps (drizzle-kit needed for migrations at runtime) RUN pnpm install --frozen-lockfile --ignore-scripts -# Copy built artifacts +# Copy built artifacts from builder COPY --from=builder /app/dist ./dist -COPY --from=builder /app/drizzle ./drizzle -# Copy drizzle config for migrations (needed by drizzle-kit at runtime) +# Copy drizzle config and migrations for runtime migration COPY drizzle.config.ts ./drizzle.config.ts +COPY drizzle/ ./drizzle/ # Copy entrypoint script COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -# Set ownership -RUN chown -R goclaw:goclaw /app +# Run as non-root user +RUN addgroup -g 1001 goclaw && \ + adduser -u 1001 -G goclaw -s /bin/sh -D goclaw && \ + chown -R goclaw:goclaw /app USER goclaw diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ad0bf26..5137a85 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -34,7 +34,6 @@ volumes: # ollama-data: # Uncomment when using local Ollama service below services: - # ── MySQL 8 ────────────────────────────────────────────────────────────── db: image: mysql:8.0 @@ -52,7 +51,17 @@ services: networks: - goclaw-net healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-goClawRoot123}"] + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "root", + "-p${MYSQL_ROOT_PASSWORD:-goClawRoot123}", + ] interval: 10s timeout: 5s retries: 5 @@ -99,7 +108,7 @@ services: # ── LLM Provider ───────────────────────────────────────────────────── # Cloud default (Ollama Cloud, OpenAI-compatible): LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}" - LLM_API_KEY: "${LLM_API_KEY:-${OLLAMA_API_KEY:-}}" + LLM_API_KEY: "${LLM_API_KEY:-${OLLAMA_API_KEY:-}}" # Legacy alias (still supported): OLLAMA_API_KEY: "${OLLAMA_API_KEY:-${LLM_API_KEY:-}}" # ── To use local Ollama on GPU node, set in .env: ───────────────────── @@ -145,6 +154,8 @@ services: DATABASE_URL: "mysql://${MYSQL_USER:-goclaw}:${MYSQL_PASSWORD:-goClawPass123}@db:3306/${MYSQL_DATABASE:-goclaw}" GATEWAY_URL: "http://gateway:18789" JWT_SECRET: "${JWT_SECRET:-change-me-in-production}" + OLLAMA_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}" + OLLAMA_API_KEY: "${LLM_API_KEY:-}" VITE_APP_ID: "${VITE_APP_ID:-}" OAUTH_SERVER_URL: "${OAUTH_SERVER_URL:-}" VITE_OAUTH_PORTAL_URL: "${VITE_OAUTH_PORTAL_URL:-}" diff --git a/docker/docker-stack.yml b/docker/docker-stack.yml index db5ecf8..1e64a2f 100644 --- a/docker/docker-stack.yml +++ b/docker/docker-stack.yml @@ -40,31 +40,15 @@ volumes: # ollama-data: # Uncomment when using local Ollama service below # driver: local -secrets: - mysql-root-password: - external: true - mysql-password: - external: true - jwt-secret: - external: true - llm-api-key: - external: true - services: - # ── MySQL 8 ────────────────────────────────────────────────────────────── db: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql-root-password + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-goClawRoot123}" MYSQL_DATABASE: goclaw MYSQL_USER: goclaw - MYSQL_PASSWORD_FILE: /run/secrets/mysql-password - secrets: - - mysql-root-password - - mysql-password - volumes: - - mysql-data:/var/lib/mysql + MYSQL_PASSWORD: "${MYSQL_PASSWORD:-goClawPass123}" networks: - goclaw-net deploy: @@ -123,23 +107,18 @@ services: environment: PORT: "18789" # ── LLM Provider ───────────────────────────────────────────────────── - # Default: Ollama Cloud (requires llm-api-key secret below) + # Default: Ollama Cloud (requires LLM_API_KEY) LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}" + LLM_API_KEY: "${LLM_API_KEY:-}" DEFAULT_MODEL: "${DEFAULT_MODEL:-qwen2.5:7b}" - # LLM_API_KEY is injected via /run/secrets/llm-api-key (see below) # ── To switch to local GPU Ollama, set: ────────────────────────────── # LLM_BASE_URL: "http://ollama:11434" - # (and uncomment the ollama service above) # ───────────────────────────────────────────────────────────────────── - DATABASE_URL: "goclaw:{{MYSQL_PASSWORD}}@tcp(db:3306)/goclaw?parseTime=true" + DATABASE_URL: "goclaw:goClawPass123@tcp(db:3306)/goclaw?parseTime=true" PROJECT_ROOT: "/app" GATEWAY_REQUEST_TIMEOUT_SECS: "120" GATEWAY_MAX_TOOL_ITERATIONS: "10" LOG_LEVEL: "info" - secrets: - - mysql-password - - source: llm-api-key - target: /run/secrets/llm-api-key networks: - goclaw-net ports: @@ -183,11 +162,11 @@ services: image: git.softuniq.eu/uniqai/goclaw/control-center:latest environment: NODE_ENV: production - DATABASE_URL: "mysql://goclaw:{{MYSQL_PASSWORD}}@db:3306/goclaw" + DATABASE_URL: "mysql://goclaw:goClawPass123@db:3306/goclaw" GATEWAY_URL: "http://gateway:18789" - secrets: - - mysql-password - - jwt-secret + JWT_SECRET: "${JWT_SECRET:-change-me-in-production}" + LLM_BASE_URL: "${LLM_BASE_URL:-https://ollama.com/v1}" + LLM_API_KEY: "${LLM_API_KEY:-}" networks: - goclaw-net ports: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 491d3eb..44d4ba3 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,18 +1,48 @@ #!/bin/sh # GoClaw Control Center entrypoint -# Runs Drizzle migrations before starting the server +# Runs database migrations before starting the server set -e -echo "[Entrypoint] Running database migrations..." +echo "[Entrypoint] Waiting for database connectivity..." + +# Wait for database to be reachable (max 30 attempts = 60 seconds) +DB_READY=false +ATTEMPT=0 +MAX_ATTEMPTS=30 + +# Parse DATABASE_URL for connection check +# Supports: mysql://user:pass@host:port/dbname +if [ -n "$DATABASE_URL" ]; then + # Extract host and port from DATABASE_URL + DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:/]+)(:[0-9]+)?/.*|\1|') + DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*@[^:/]+:([0-9]+)/.*|\1|') + if [ -z "$DB_PORT" ]; then DB_PORT=3306; fi + + while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + ATTEMPT=$((ATTEMPT + 1)) + if nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null || timeout 2 sh -c "echo > /dev/tcp/$DB_HOST/$DB_PORT" 2>/dev/null; then + DB_READY=true + echo "[Entrypoint] Database is reachable (attempt $ATTEMPT/$MAX_ATTEMPTS)" + break + fi + echo "[Entrypoint] Waiting for database at $DB_HOST:$DB_PORT... (attempt $ATTEMPT/$MAX_ATTEMPTS)" + sleep 2 + done +fi + +if [ "$DB_READY" = "false" ] && [ -n "$DATABASE_URL" ]; then + echo "[Entrypoint] WARNING: Database not reachable after $MAX_ATTEMPTS attempts. Continuing anyway..." +fi + +echo "[Entrypoint] Running database migrations (via drizzle-kit)..." # Run drizzle-kit migrate — applies all pending migrations idempotently -# drizzle.config.ts and drizzle/migrations/ are copied into /app during build -cd /app && node_modules/.bin/drizzle-kit migrate 2>&1 +cd /app && npx drizzle-kit migrate 2>&1 MIGRATE_EXIT=$? if [ $MIGRATE_EXIT -ne 0 ]; then - echo "[Entrypoint] WARNING: Migration failed (exit $MIGRATE_EXIT). Starting server anyway..." + echo "[Entrypoint] WARNING: Migration failed (exit $MIGRATE_EXIT). Starting server anyway — seed will handle schema..." else echo "[Entrypoint] Migrations applied successfully." fi diff --git a/drizzle.config.ts b/drizzle.config.ts index 321752c..7843951 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -12,4 +12,7 @@ export default defineConfig({ dbCredentials: { url: connectionString, }, + migrations: { + prefix: "timestamp", + }, }); diff --git a/server/_core/index.ts b/server/_core/index.ts index 4a38e6e..9f0a0a9 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -34,6 +34,16 @@ async function startServer() { // Configure body parser with larger size limit for file uploads app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); + + // Health check endpoint for Docker HEALTHCHECK and load balancers + app.get("/api/health", (_req, res) => { + res.json({ + status: "ok", + uptime: Math.floor(process.uptime()), + timestamp: new Date().toISOString(), + }); + }); + // OAuth callback under /api/oauth/callback registerOAuthRoutes(app); // tRPC API diff --git a/server/_core/vite.ts b/server/_core/vite.ts index 86d07f6..5ad3f42 100644 --- a/server/_core/vite.ts +++ b/server/_core/vite.ts @@ -48,10 +48,9 @@ export async function setupVite(app: Express, server: Server) { } export function serveStatic(app: Express) { - const distPath = - process.env.NODE_ENV === "development" - ? path.resolve(import.meta.dirname, "../..", "dist", "public") - : path.resolve(import.meta.dirname, "public"); + // dist/index.js is the server bundle, dist/public/ is the client build + // import.meta.dirname = .../dist → public is at dist/public + const distPath = path.resolve(import.meta.dirname, "public"); if (!fs.existsSync(distPath)) { console.error( `Could not find the build directory: ${distPath}, make sure to build the client first`