diff --git a/build_image.sh b/build_image.sh index b0189ba05..196d0034a 100755 --- a/build_image.sh +++ b/build_image.sh @@ -83,8 +83,8 @@ docker buildx build --network=host ${PUSH_FLAG} --platform linux/amd64,linux/arm # Build frontend image docker buildx build --network=host ${PUSH_FLAG} --platform linux/amd64,linux/arm64 -t ghcr.io/wecode-ai/wegent-web:${VERSION} -f docker/frontend/Dockerfile . -# Build executor image -docker buildx build --network=host ${PUSH_FLAG} --platform linux/amd64,linux/arm64 -t ghcr.io/wecode-ai/wegent-executor:${VERSION} -f docker/executor/Dockerfile . +# Build executor image (default target: binary) +docker buildx build --network=host ${PUSH_FLAG} --platform linux/amd64,linux/arm64 --target binary -t ghcr.io/wecode-ai/wegent-executor:${VERSION} -f docker/executor/Dockerfile . # Build executor manager image docker buildx build --network=host ${PUSH_FLAG} --platform linux/amd64,linux/arm64 -t ghcr.io/wecode-ai/wegent-executor-manager:${VERSION} -f docker/executor_manager/Dockerfile . diff --git a/build_image_mac.sh b/build_image_mac.sh index 7ae6ff383..ac5547d95 100755 --- a/build_image_mac.sh +++ b/build_image_mac.sh @@ -83,8 +83,8 @@ docker build --network=host ${PUSH_FLAG} -t ghcr.io/wecode-ai/wegent-backend:${V # Build frontend image docker build --network=host ${PUSH_FLAG} -t ghcr.io/wecode-ai/wegent-web:${VERSION} -f docker/frontend/Dockerfile . -# Build executor image -docker build --network=host ${PUSH_FLAG} -t ghcr.io/wecode-ai/wegent-executor:${VERSION} -f docker/executor/Dockerfile . +# Build executor image (default target: binary) +docker build --network=host ${PUSH_FLAG} --target binary -t ghcr.io/wecode-ai/wegent-executor:${VERSION} -f docker/executor/Dockerfile . # Build executor manager image docker build --network=host ${PUSH_FLAG} -t ghcr.io/wecode-ai/wegent-executor-manager:${VERSION} -f docker/executor_manager/Dockerfile . diff --git a/docker/executor/Dockerfile b/docker/executor/Dockerfile index 5d8bbddca..bb75c4021 100644 --- a/docker/executor/Dockerfile +++ b/docker/executor/Dockerfile @@ -2,28 +2,10 @@ # # SPDX-License-Identifier: Apache-2.0 -# Build stage: compile executor to standalone binary -FROM ghcr.io/wecode-ai/wegent-base-python3.12:latest AS builder - -WORKDIR /app - -# Copy pyproject.toml and install dependencies -COPY executor/pyproject.toml /app/executor/pyproject.toml - -RUN cd /app/executor && uv pip install --system --no-cache -r pyproject.toml - -# Install PyInstaller for building standalone binary (using uv for speed) -RUN uv pip install --system pyinstaller - -# Copy source code -COPY executor /app/executor -COPY shared /app/shared - -# Build standalone binary -RUN cd /app/executor && bash build.sh - -# Runtime stage: minimal image with only the binary -FROM ghcr.io/wecode-ai/wegent-base-python3.12:latest +# ============================================================================ +# Stage 1: deps — system packages + Python dependencies (shared by all targets) +# ============================================================================ +FROM ghcr.io/wecode-ai/wegent-base-python3.12:latest AS deps WORKDIR /app @@ -42,14 +24,12 @@ RUN dnf install -y \ && rm -rf /var/cache/dnf/* # Install Claude marketplace skills and npm packages for document generation -# Install to /root so it's available in the base image RUN claude plugin marketplace add anthropics/skills \ && npm install -g pptxgenjs playwright sharp react react-dom react-icons docx pdf-lib \ && npx playwright install chromium \ && npm cache clean --force # Install Python packages for document generation -# These are commonly used by document skills RUN pip install python-pptx openpyxl python-docx reportlab Pillow pandas \ pypdf pdfplumber pytesseract \ "markitdown[pptx]" defusedxml weasyprint \ @@ -66,13 +46,44 @@ RUN mkdir -p /usr/share/fonts/truetype/noto && \ -o /usr/share/fonts/truetype/noto/NotoSansSC.ttf) && \ fc-cache -f -v - -# Copy only the standalone binary from builder -COPY --from=builder /app/executor/dist/executor /app/executor +# Install Python executor dependencies (cached unless pyproject.toml changes) +COPY executor/pyproject.toml /app/executor/pyproject.toml +RUN cd /app/executor && uv pip install --system --no-cache -r pyproject.toml ENV PORT=10001 ENV PYTHONPATH=/app ENV NODE_PATH=/usr/lib/node_modules -# Run the standalone binary +# ============================================================================ +# Stage 2: builder — compiles executor Python source to standalone binary +# ============================================================================ +FROM deps AS builder + +# Install PyInstaller +RUN uv pip install --system pyinstaller + +# Copy source code and build binary +COPY executor /app/executor +COPY shared /app/shared +RUN cd /app/executor && bash build.sh + +# ============================================================================ +# Stage 3: source — dev target, runs Python source directly (no compilation) +# docker build --target source -t wegent-executor:local-dev . +# ============================================================================ +FROM deps AS source + +COPY executor /app/executor +COPY shared /app/shared + +CMD ["python", "/app/executor/main.py"] + +# ============================================================================ +# Stage 4: binary — default target, runs compiled standalone binary +# docker build -t wegent-executor:latest . +# ============================================================================ +FROM deps AS binary + +COPY --from=builder /app/executor/dist/executor /app/executor + CMD ["/app/executor"] diff --git a/frontend/next.config.js b/frontend/next.config.js index 61e2dd684..608c5b8f2 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -5,8 +5,12 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports const path = require('path') -// Check if running with Turbopack (development mode with --turbopack flag) -const isTurbopack = process.env.TURBOPACK === '1' +// Resolve path with POSIX-style separators for WSL/Windows compatibility +// Using path.resolve can produce Windows-style paths (D:\...) in WSL environments +// which webpack cannot handle. Converting to forward slashes fixes this. +function resolveAlias(relativePath) { + return path.resolve(__dirname, relativePath).split(path.sep).join('/') +} /** @type {import('next').NextConfig} */ const nextConfig = { @@ -14,7 +18,7 @@ const nextConfig = { output: 'standalone', // Allow cross-origin requests in development mode // This prevents "Cross origin request detected" warning - allowedDevOrigins: ['localhost:3000'], + allowedDevOrigins: ['localhost', '127.0.0.1'], // Transpile node_modules that ship modern JS syntax for iOS 16 Safari compatibility transpilePackages: [ 'mermaid', @@ -30,60 +34,63 @@ const nextConfig = { '@replit/codemirror-vim', 'katex', ], - // Webpack configuration for production builds - // Note: In development mode with Turbopack, this is not used - // The warning "Webpack is configured while Turbopack is not" can be safely ignored - // as these optimizations are primarily for production builds which use webpack - ...(isTurbopack - ? {} - : { - webpack: (config, { isServer, _dev }) => { - // Force replace remark-gfm with our iOS 16 compatible version - // This is needed because @uiw/react-md-editor depends on remark-gfm - // which uses lookbehind regex not supported by iOS 16 - config.resolve.alias = { - ...config.resolve.alias, - 'remark-gfm': path.resolve(__dirname, 'src/lib/remark-gfm-safe.ts'), - } + // Turbopack configuration (development mode) + // Mirrors the remark-gfm alias from webpack config for iOS 16 Safari compatibility + // NOTE: Turbopack resolveAlias requires a project-relative path (starting with ./) + // NOT an absolute path — absolute paths (even with forward slashes) are treated as + // external modules which causes "chunking context does not support external modules" + turbopack: { + resolveAlias: { + 'remark-gfm': './src/lib/remark-gfm-safe.ts', + }, + }, + // Webpack configuration (production builds) + webpack: (config, { isServer, _dev }) => { + // Force replace remark-gfm with our iOS 16 compatible version + // This is needed because @uiw/react-md-editor depends on remark-gfm + // which uses lookbehind regex not supported by iOS 16 + config.resolve.alias = { + ...config.resolve.alias, + 'remark-gfm': resolveAlias('src/lib/remark-gfm-safe.ts'), + } - // Handle chunk loading issues - config.optimization = { - ...config.optimization, - // Prevent over-aggressive code splitting that can cause chunk loading errors - splitChunks: { - ...config.optimization?.splitChunks, - chunks: 'all', - cacheGroups: { - vendor: { - test: /[\\/]node_modules[\\/]/, - name: 'vendors', - chunks: 'all', - priority: 10, - }, - common: { - name: 'common', - minChunks: 2, - chunks: 'all', - priority: 5, - }, - }, - }, - // Enable module concatenation to reduce bundle size - concatenateModules: true, - } + // Handle chunk loading issues + config.optimization = { + ...config.optimization, + // Prevent over-aggressive code splitting that can cause chunk loading errors + splitChunks: { + ...config.optimization?.splitChunks, + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + chunks: 'all', + priority: 10, + }, + common: { + name: 'common', + minChunks: 2, + chunks: 'all', + priority: 5, + }, + }, + }, + // Enable module concatenation to reduce bundle size + concatenateModules: true, + } - // Handle dynamic imports more gracefully - if (!isServer) { - config.resolve.fallback = { - ...config.resolve.fallback, - fs: false, - path: false, - } - } + // Handle dynamic imports more gracefully + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + } + } - return config - }, - }), + return config + }, // Experimental features to improve stability experimental: { // Disable CSS chunking to fix Safari/iOS bug where CSS files diff --git a/frontend/package.json b/frontend/package.json index cfc58cdc2..172e566ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "TURBOPACK=1 next dev --turbopack", + "dev": "next dev --turbopack", "build": "NODE_OPTIONS='--max-old-space-size=2048' next build", "start": "next start", "lint": "next lint", diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js index a947f7aa6..4e3232844 100644 --- a/frontend/public/mockServiceWorker.js +++ b/frontend/public/mockServiceWorker.js @@ -1,3 +1,4 @@ + /* tslint:disable */ /** diff --git a/shared/utils/git_util.py b/shared/utils/git_util.py index 8ec7f1192..3b01e3876 100644 --- a/shared/utils/git_util.py +++ b/shared/utils/git_util.py @@ -72,9 +72,19 @@ def clone_repo(project_url, branch, project_path, user_name=None, token=None): f"get git token from url: {project_url}, branch:{branch}, project:{project_path}" ) if token: - return clone_repo_with_token( + success, error = clone_repo_with_token( project_url, branch, project_path, user_name, token ) + # If http:// clone failed, retry with https:// in case the server enforces HTTPS + if not success and project_url.startswith("http://"): + https_url = "https://" + project_url[7:] + logger.info( + f"http clone failed, retrying with https: {https_url}" + ) + success, error = clone_repo_with_token( + https_url, branch, project_path, user_name, token + ) + return success, error return False, "Token is not provided" diff --git a/start.sh b/start.sh index f7ec2567a..547fd4db8 100755 --- a/start.sh +++ b/start.sh @@ -360,64 +360,102 @@ show_docker_install_instructions() { } # Check if MySQL and Redis are running -check_mysql_redis() { - local mysql_running=false - local redis_running=false +# Probe TCP connectivity to host:port, return 0 if reachable +probe_tcp() { + local host=$1 + local port=$2 + # Try bash /dev/tcp first (no extra tools needed), fall back to nc + if (echo > /dev/tcp/"$host"/"$port") 2>/dev/null; then + return 0 + elif command -v nc >/dev/null 2>&1 && nc -z -w2 "$host" "$port" 2>/dev/null; then + return 0 + fi + return 1 +} - # Check if MySQL container is running - if docker ps --format '{{.Names}}' | grep -q "^wegent-mysql$"; then - mysql_running=true +# Extract host and port from DATABASE_URL (mysql+pymysql://user:pass@host:port/db) +get_db_host_port() { + local url="${DATABASE_URL:-}" + if [ -z "$url" ]; then + # Fall back to compose defaults + echo "127.0.0.1 ${MYSQL_PORT:-3306}" + return + fi + local hostport + hostport=$(echo "$url" | sed -E 's|.*@([^/]+)/.*|\1|') + local host port + host=$(echo "$hostport" | cut -d: -f1) + port=$(echo "$hostport" | cut -d: -f2) + # Resolve docker container name to localhost when running outside docker + if [ "$host" != "localhost" ] && [ "$host" != "127.0.0.1" ]; then + host="127.0.0.1" fi + echo "$host ${port:-3306}" +} - # Check if Redis container is running - if docker ps --format '{{.Names}}' | grep -q "^wegent-redis$"; then - redis_running=true +# Extract host and port from REDIS_URL (redis://:pass@host:port/db) +get_redis_host_port() { + local url="${REDIS_URL:-}" + if [ -z "$url" ]; then + echo "127.0.0.1 ${REDIS_PORT:-6379}" + return + fi + local hostport + hostport=$(echo "$url" | sed -E 's|.*@([^/]+)(/.*)?$|\1|') + local host port + host=$(echo "$hostport" | cut -d: -f1) + port=$(echo "$hostport" | cut -d: -f2) + if [ "$host" != "localhost" ] && [ "$host" != "127.0.0.1" ]; then + host="127.0.0.1" fi + echo "$host ${port:-6379}" +} - if [ "$mysql_running" = true ] && [ "$redis_running" = true ]; then - echo -e "${GREEN}✓ MySQL and Redis are already running${NC}" +check_mysql_redis() { + local db_host db_port redis_host redis_port + read -r db_host db_port <<< "$(get_db_host_port)" + read -r redis_host redis_port <<< "$(get_redis_host_port)" + + local mysql_reachable=false + local redis_reachable=false + + if probe_tcp "$db_host" "$db_port"; then + mysql_reachable=true + fi + if probe_tcp "$redis_host" "$redis_port"; then + redis_reachable=true + fi + + if [ "$mysql_reachable" = true ] && [ "$redis_reachable" = true ]; then + echo -e "${GREEN}✓ MySQL ($db_host:$db_port) and Redis ($redis_host:$redis_port) are reachable${NC}" return 0 fi - # Start MySQL and Redis if not running - echo -e "${YELLOW}MySQL or Redis is not running. Starting them with docker-compose...${NC}" - + # One or both are not reachable — try starting wegent's own containers + echo -e "${YELLOW}MySQL or Redis is not reachable. Starting them with docker-compose...${NC}" + if ! docker compose up -d mysql redis; then echo -e "${RED}Error: Failed to start MySQL and Redis${NC}" echo -e "${YELLOW}Please check docker-compose.yml and ensure Docker is running${NC}" exit 1 fi - # Wait for services to be healthy + # Wait until both ports become reachable (up to 60s) echo -e "${YELLOW}Waiting for MySQL and Redis to be ready...${NC}" local max_wait=60 local waited=0 - + while [ $waited -lt $max_wait ]; do - local mysql_healthy=false - local redis_healthy=false - - # Check MySQL health - if docker inspect wegent-mysql --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then - mysql_healthy=true - fi - - # Check Redis health - if docker inspect wegent-redis --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then - redis_healthy=true - fi - - if [ "$mysql_healthy" = true ] && [ "$redis_healthy" = true ]; then + if probe_tcp "$db_host" "$db_port" && probe_tcp "$redis_host" "$redis_port"; then echo -e "${GREEN}✓ MySQL and Redis are ready${NC}" return 0 fi - sleep 2 waited=$((waited + 2)) echo -e " Waiting... (${waited}s/${max_wait}s)" done - - echo -e "${RED}Error: MySQL or Redis failed to become healthy within ${max_wait}s${NC}" + + echo -e "${RED}Error: MySQL or Redis failed to become reachable within ${max_wait}s${NC}" echo -e "${YELLOW}You can check the logs with:${NC}" echo -e " ${BLUE}docker logs wegent-mysql${NC}" echo -e " ${BLUE}docker logs wegent-redis${NC}" @@ -692,6 +730,7 @@ Options: -p, --port PORT Frontend port (default: $DEFAULT_WEGENT_FRONTEND_PORT) -e, --executor-image IMG Executor image (default: $DEFAULT_EXECUTOR_IMAGE) --socket-url URL Socket direct url (auto-computed from BACKEND_PORT) + --backend Start backend services only (skip frontend) --init Interactive configuration initialization --stop Stop all services --restart Restart all services @@ -727,6 +766,7 @@ Examples: $0 -e my-executor:latest # Specify custom executor image $0 --socket-url http://192.168.1.100:8000 # Specify socket URL with your IP $0 --stop # Stop all services + $0 --backend # Start backend only (run frontend separately) EOF } @@ -736,6 +776,7 @@ ACTION="start" # Track which variables were set via command line (to override config file) CLI_BACKEND_PORT="" +CLI_BACKEND_ONLY=false CLI_CHAT_SHELL_PORT="" CLI_EXECUTOR_MANAGER_PORT="" CLI_WEGENT_FRONTEND_PORT="" @@ -748,6 +789,10 @@ case $1 in CLI_BACKEND_PORT="$2" shift 2 ;; + --backend) + CLI_BACKEND_ONLY=true + shift + ;; -c|--chat-shell-port) CLI_CHAT_SHELL_PORT="$2" shift 2 @@ -885,7 +930,8 @@ check_socket_url_ip() { # Check all required ports check_all_ports() { - local ports=("$BACKEND_PORT:Backend" "$CHAT_SHELL_PORT:Chat Shell" "$EXECUTOR_MANAGER_PORT:Executor Manager" "$WEGENT_FRONTEND_PORT:Frontend") + local ports=("$BACKEND_PORT:Backend" "$CHAT_SHELL_PORT:Chat Shell" "$EXECUTOR_MANAGER_PORT:Executor Manager") + [ "$CLI_BACKEND_ONLY" = false ] && ports+=("$WEGENT_FRONTEND_PORT:Frontend") local conflicts=() for item in "${ports[@]}"; do @@ -1031,7 +1077,7 @@ check_service_health() { local name=$1 local port=$2 local health_path=$3 - local max_retries=15 + local max_retries=${4:-15} local retry_interval=2 echo -n " Checking $name..." @@ -1121,11 +1167,13 @@ start_services() { check_libmagic_installed echo -e " ${GREEN}✓${NC} libmagic detected" - # Check Node.js - check_node_installed - local node_version=$(node --version 2>&1) - local npm_version=$(npm --version 2>&1) - echo -e " ${GREEN}✓${NC} Node.js detected: $node_version (npm $npm_version)" + # Check Node.js (skipped in --backend mode) + if [ "$CLI_BACKEND_ONLY" = false ]; then + check_node_installed + local node_version=$(node --version 2>&1) + local npm_version=$(npm --version 2>&1) + echo -e " ${GREEN}✓${NC} Node.js detected: $node_version (npm $npm_version)" + fi echo "" echo -e "${GREEN}Configuration:${NC}" @@ -1163,10 +1211,12 @@ start_services() { check_python_env "chat_shell" "Chat Shell" echo "" - # Check frontend dependencies - echo -e "${BLUE}Checking frontend dependencies...${NC}" - check_frontend_dependencies - echo "" + # Check frontend dependencies (skipped in --backend mode) + if [ "$CLI_BACKEND_ONLY" = false ]; then + echo -e "${BLUE}Checking frontend dependencies...${NC}" + check_frontend_dependencies + echo "" + fi echo -e "${BLUE}Starting services...${NC}" @@ -1200,53 +1250,62 @@ start_services() { start_service "executor_manager" "executor_manager" \ "export EXECUTOR_IMAGE=$EXECUTOR_IMAGE && export TASK_API_DOMAIN=$TASK_API_DOMAIN && export DOCKER_HOST_ADDR=localhost && export NETWORK=wegent-network && export CALLBACK_HOST=$CALLBACK_HOST && source .venv/bin/activate && uvicorn main:app --reload --reload-dir . --reload-dir ../shared $RELOAD_EXCLUDE --host 0.0.0.0 --port $EXECUTOR_MANAGER_PORT --log-level debug" - # 4. Start Frontend (run in background) - echo -e " Starting ${BLUE}frontend${NC}..." - cd "$SCRIPT_DIR/frontend" - - # Set environment variables (use same names as docker-compose) - export RUNTIME_INTERNAL_API_URL=http://localhost:$BACKEND_PORT - export RUNTIME_SOCKET_DIRECT_URL=$WEGENT_SOCKET_URL - - # Build the frontend startup command - # In WSL, use full path to node to ensure we use the correct nvm-installed version - # instead of potentially using Windows node or a different version - local frontend_cmd="PORT=$WEGENT_FRONTEND_PORT npm run dev" - - if is_wsl; then - # Get the full path to node from the current shell (which has nvm loaded) - local node_path=$(command -v node) - if [ -n "$node_path" ]; then - # Set PATH to use the nvm node directory - local node_dir=$(dirname "$node_path") - frontend_cmd="PATH=$node_dir:\$PATH $frontend_cmd" + # 4. Start Frontend (skipped in --backend mode) + if [ "$CLI_BACKEND_ONLY" = false ]; then + echo -e " Starting ${BLUE}frontend${NC}..." + cd "$SCRIPT_DIR/frontend" + + # Set environment variables (use same names as docker-compose) + export RUNTIME_INTERNAL_API_URL=http://localhost:$BACKEND_PORT + export RUNTIME_SOCKET_DIRECT_URL=$WEGENT_SOCKET_URL + + # Build the frontend startup command + # In WSL, use full path to node to ensure we use the correct nvm-installed version + # instead of potentially using Windows node or a different version + local frontend_cmd="PORT=$WEGENT_FRONTEND_PORT npm run dev" + + if is_wsl; then + # Get the full path to node from the current shell (which has nvm loaded) + local node_path=$(command -v node) + if [ -n "$node_path" ]; then + # Set PATH to use the nvm node directory + local node_dir=$(dirname "$node_path") + frontend_cmd="PATH=$node_dir:\$PATH $frontend_cmd" + fi fi - fi - # Start frontend in background - nohup bash -c "$frontend_cmd" > "$PID_DIR/frontend.log" 2>&1 & - local frontend_pid=$! - echo $frontend_pid > "$PID_DIR/frontend.pid" + # Start frontend in background + nohup bash -c "$frontend_cmd" > "$PID_DIR/frontend.log" 2>&1 & + local frontend_pid=$! + echo $frontend_pid > "$PID_DIR/frontend.pid" + + sleep 3 - sleep 3 + if kill -0 "$frontend_pid" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} frontend started (PID: $frontend_pid)" + else + echo -e " ${RED}✗${NC} frontend failed to start, check log: $PID_DIR/frontend.log" + fi - if kill -0 "$frontend_pid" 2>/dev/null; then - echo -e " ${GREEN}✓${NC} frontend started (PID: $frontend_pid)" + cd "$SCRIPT_DIR" else - echo -e " ${RED}✗${NC} frontend failed to start, check log: $PID_DIR/frontend.log" + echo -e " ${YELLOW}--backend mode: skipping frontend${NC}" fi - cd "$SCRIPT_DIR" - echo "" echo -e "${BLUE}Performing health checks...${NC}" # Health check for all services local failed=0 - check_service_health "backend" $BACKEND_PORT "/health" || failed=1 + # Backend needs more time: runs DB migrations + YAML init + Celery startup (~90s) + check_service_health "backend" $BACKEND_PORT "/health" 60 || failed=1 check_service_health "chat_shell" $CHAT_SHELL_PORT "/health" || failed=1 check_service_health "executor_manager" $EXECUTOR_MANAGER_PORT "/health" || failed=1 - check_service_health "frontend" $WEGENT_FRONTEND_PORT "" || failed=1 + # Frontend health check (skipped in --backend mode) + if [ "$CLI_BACKEND_ONLY" = false ]; then + # Frontend needs more time: Next.js Turbopack compilation (~60s) + check_service_health "frontend" $WEGENT_FRONTEND_PORT "" 60 || failed=1 + fi echo "" if [ $failed -eq 1 ]; then @@ -1267,11 +1326,11 @@ start_services() { echo "" echo -e "${GREEN}🌐 Access URLs:${NC}" echo -e " Local Frontend: ${BLUE}http://localhost:$WEGENT_FRONTEND_PORT${NC}" - echo -e " Remote Frontend: ${BLUE}http://$(get_local_ip):$WEGENT_FRONTEND_PORT${NC}" + echo -e " Remote Frontend: ${BLUE}http://$LOCAL_IP:$WEGENT_FRONTEND_PORT${NC}" echo -e " Socket URL: ${BLUE}$WEGENT_SOCKET_URL${NC}" echo "" echo -e "${YELLOW}📋 Share with others for remote access:${NC}" - echo -e " Frontend URL: ${BLUE}http://$(get_local_ip):$WEGENT_FRONTEND_PORT${NC}" + echo -e " Frontend URL: ${BLUE}http://$LOCAL_IP:$WEGENT_FRONTEND_PORT${NC}" echo -e " Socket URL: ${BLUE}$WEGENT_SOCKET_URL${NC}" echo "" echo -e "${YELLOW}Common Commands:${NC}"