#!/bin/sh set -e CONFIG_FILE="${APP_HOME}/config.yaml" # Priority 1: Use USERNAME/PASSWORD if both are provided if [ -n "${USERNAME}" ] && [ -n "${PASSWORD}" ]; then echo "--- Basic auth enabled: Creating config.yaml with provided credentials. ---" cat < ${CONFIG_FILE} dataRoot: ./data listen: true listenAddress: ipv4: 0.0.0.0 ipv6: '[::]' protocol: ipv4: true ipv6: false dnsPreferIPv6: false autorunHostname: "auto" port: 8000 autorunPortOverride: -1 ssl: enabled: false certPath: "./certs/cert.pem" keyPath: "./certs/privkey.pem" whitelistMode: false enableForwardedWhitelist: false whitelist: - ::1 - 127.0.0.1 whitelistDockerHosts: true basicAuthMode: true basicAuthUser: username: "${USERNAME}" password: "${PASSWORD}" enableCorsProxy: false requestProxy: enabled: false url: "socks5://username:password@example.com:1080" bypass: - localhost - 127.0.0.1 enableUserAccounts: false enableDiscreetLogin: false autheliaAuth: false perUserBasicAuth: false sessionTimeout: -1 disableCsrfProtection: false securityOverride: false logging: enableAccessLog: true minLogLevel: 0 rateLimiting: preferRealIpHeader: false autorun: false avoidLocalhost: false backups: common: numberOfBackups: 50 chat: enabled: true checkIntegrity: true maxTotalBackups: -1 throttleInterval: 10000 thumbnails: enabled: true format: "jpg" quality: 95 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] } performance: lazyLoadCharacters: false memoryCacheCapacity: '100mb' useDiskCache: true allowKeysExposure: true skipContentCheck: false whitelistImportDomains: - localhost - cdn.discordapp.com - files.catbox.moe - raw.githubusercontent.com requestOverrides: [] extensions: enabled: true autoUpdate: false models: autoDownload: true classification: Cohee/distilbert-base-uncased-go-emotions-onnx captioning: Xenova/vit-gpt2-image-captioning embedding: Cohee/jina-embeddings-v2-base-en speechToText: Xenova/whisper-small textToSpeech: Xenova/speecht5_tts enableDownloadableTokenizers: true promptPlaceholder: "[Start a new chat]" openai: randomizeUserId: false captionSystemPrompt: "" deepl: formality: default mistral: enablePrefix: false ollama: keepAlive: -1 batchSize: -1 claude: enableSystemPromptCache: false cachingAtDepth: -1 enableServerPlugins: true enableServerPluginsAutoUpdate: false EOT # Priority 2: Use CONFIG_YAML if provided (and username/password are not) elif [ -n "${CONFIG_YAML}" ]; then echo "--- Found CONFIG_YAML, creating config.yaml from environment variable. ---" printf '%s\n' "${CONFIG_YAML}" > ${CONFIG_FILE} # Priority 3: No config provided, let the app use its defaults else echo "--- No user/pass or CONFIG_YAML provided. App will use its default settings. ---" fi # --- BEGIN: Update SillyTavern Core at Runtime --- echo '--- Attempting to update SillyTavern Core from GitHub (staging branch) ---' if [ -d ".git" ] && [ "$(git rev-parse --abbrev-ref HEAD)" = "staging" ]; then echo 'Existing staging branch found. Resetting and pulling latest changes...' git reset --hard HEAD && \ git pull origin staging || echo 'WARN: git pull failed, continuing with code from build time.' echo '--- SillyTavern Core update check finished. ---' else echo 'WARN: .git directory not found or not on staging branch. Skipping runtime update. Code from build time will be used.' fi # --- END: Update SillyTavern Core at Runtime --- # --- BEGIN: Configure Git default identity at Runtime --- echo '--- Configuring Git default user identity at runtime ---' git config --global user.name "SillyTavern Sync" && \ git config --global user.email "sillytavern-sync@example.com" && \ git config --global --add safe.directory "${APP_HOME}/data" echo '--- Git identity configured for runtime user. ---' # --- END: Configure Git default identity at Runtime --- # --- BEGIN: Dynamically Install Plugins at Runtime --- echo '--- Checking for PLUGINS environment variable ---' if [ -n "$PLUGINS" ]; then echo "*** Installing Plugins specified in PLUGINS environment variable: $PLUGINS ***" # Ensure plugins directory exists mkdir -p ./plugins && chown node:node ./plugins # Set comma as delimiter IFS=',' # Loop through each plugin URL for plugin_url in $PLUGINS; do # Trim leading/trailing whitespace plugin_url=$(echo "$plugin_url" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -z "$plugin_url" ]; then continue; fi # Extract plugin name plugin_name_git=$(basename "$plugin_url") plugin_name=${plugin_name_git%.git} plugin_dir="./plugins/$plugin_name" echo "--- Installing plugin: $plugin_name from $plugin_url into $plugin_dir ---" # Remove existing dir if it exists rm -rf "$plugin_dir" # Clone the plugin (run as root, fix perms later) git clone --depth 1 "$plugin_url" "$plugin_dir" if [ -f "$plugin_dir/package.json" ]; then echo "--- Installing dependencies for $plugin_name ---" (cd "$plugin_dir" && npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev --force && npm cache clean --force) || echo "WARN: Failed to install dependencies for $plugin_name" else echo "--- No package.json found for $plugin_name, skipping dependency install. ---" fi || echo "WARN: Failed to clone $plugin_name from $plugin_url, skipping..." # Configure cloud-saves plugin if this is the cloud-saves plugin if [ "$plugin_name" = "cloud-saves" ]; then echo "--- Detected cloud-saves plugin, checking for configuration environment variables ---" # Set default values REPO_URL_VALUE=${REPO_URL:-"https://github.com/fuwei99/sillytravern"} GITHUB_TOKEN_VALUE=${GITHUB_TOKEN:-""} AUTOSAVE_INTERVAL_VALUE=${AUTOSAVE_INTERVAL:-30} AUTOSAVE_TARGET_TAG_VALUE=${AUTOSAVE_TARGET_TAG:-""} # Always set autosave to false as required AUTOSAVE_ENABLED="false" echo "--- Creating cloud-saves plugin configuration file ---" CONFIG_JSON_FILE="$plugin_dir/config.json" # Generate config.json file cat < ${CONFIG_JSON_FILE} { "repo_url": "${REPO_URL_VALUE}", "branch": "main", "username": "cloud-saves", "github_token": "${GITHUB_TOKEN_VALUE}", "display_name": "", "is_authorized": true, "last_save": null, "current_save": null, "has_temp_stash": false, "autoSaveEnabled": ${AUTOSAVE_ENABLED}, "autoSaveInterval": ${AUTOSAVE_INTERVAL_VALUE}, "autoSaveTargetTag": "${AUTOSAVE_TARGET_TAG_VALUE}" } EOT # Set correct permissions for config file chown node:node ${CONFIG_JSON_FILE} echo "--- cloud-saves plugin configuration file created at: ${CONFIG_JSON_FILE} ---" fi done # Reset IFS unset IFS # Fix permissions for plugins directory after installation echo "--- Setting permissions for plugins directory ---" chown -R node:node ./plugins echo "*** Plugin installation finished. ***" else echo 'PLUGINS environment variable is not set or empty, skipping runtime plugin installation.' fi # --- END: Dynamically Install Plugins at Runtime --- echo "*** Starting SillyTavern... ***" node ${APP_HOME}/server.js & SERVER_PID=$! echo "SillyTavern server started with PID ${SERVER_PID}. Waiting for it to become responsive..." # --- Health Check Logic --- HEALTH_CHECK_URL="http://localhost:8000/" CURL_COMMAND="curl -sf" # If basic auth is enabled, provide credentials to curl for health checks if [ -n "${USERNAME}" ] && [ -n "${PASSWORD}" ]; then echo "--- Health check will use basic auth credentials. ---" # The -u flag provides user:password for basic auth CURL_COMMAND="curl -sf -u \"${USERNAME}:${PASSWORD}\"" fi # Health check loop RETRY_COUNT=0 MAX_RETRIES=12 # Wait for 60 seconds max # Use eval to correctly execute the command string with quotes while ! eval "${CURL_COMMAND} ${HEALTH_CHECK_URL}" > /dev/null; do RETRY_COUNT=$((RETRY_COUNT+1)) if [ ${RETRY_COUNT} -ge ${MAX_RETRIES} ]; then echo "SillyTavern failed to start. Exiting." kill ${SERVER_PID} exit 1 fi echo "SillyTavern is still starting or not responsive on port 8000, waiting 5 seconds..." sleep 5 done echo "SillyTavern started successfully! Beginning periodic keep-alive..." # --- BEGIN: Install Extensions after SillyTavern startup --- install_extensions() { echo "--- Waiting 40 seconds before installing extensions... ---" sleep 40 echo "--- Checking for EXTENSIONS environment variable ---" if [ -n "$EXTENSIONS" ]; then echo "*** Installing Extensions specified in EXTENSIONS environment variable: $EXTENSIONS ***" # Determine installation directory based on INSTALL_FOR_ALL_USERS if [ "$INSTALL_FOR_ALL_USERS" = "true" ]; then # System-level installation (for all users) EXTENSIONS_DIR="./public/scripts/extensions/third-party" echo "Installing extensions for all users in: $EXTENSIONS_DIR" else # User-level installation (for default user only) EXTENSIONS_DIR="./data/default-user/extensions" echo "Installing extensions for default user in: $EXTENSIONS_DIR" fi # Ensure extensions directory exists mkdir -p "$EXTENSIONS_DIR" && chown node:node "$EXTENSIONS_DIR" # Set comma as delimiter IFS=',' # Loop through each extension URL for extension_url in $EXTENSIONS; do # Trim leading/trailing whitespace extension_url=$(echo "$extension_url" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -z "$extension_url" ]; then continue; fi # Extract extension name extension_name_git=$(basename "$extension_url") extension_name=${extension_name_git%.git} extension_dir="$EXTENSIONS_DIR/$extension_name" echo "--- Installing extension: $extension_name from $extension_url into $extension_dir ---" # Remove existing dir if it exists rm -rf "$extension_dir" # Clone the extension git clone --depth 1 "$extension_url" "$extension_dir" # Check if extension has package.json and install dependencies if needed if [ -f "$extension_dir/package.json" ]; then echo "--- Installing dependencies for $extension_name ---" (cd "$extension_dir" && npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev --force && npm cache clean --force) || echo "WARN: Failed to install dependencies for $extension_name" else echo "--- No package.json found for $extension_name, skipping dependency install. ---" fi || echo "WARN: Failed to clone $extension_name from $extension_url, skipping..." done # Reset IFS unset IFS # Fix permissions for extensions directory after installation echo "--- Setting permissions for extensions directory ---" chown -R node:node "$EXTENSIONS_DIR" echo "*** Extensions installation finished. ***" else echo 'EXTENSIONS environment variable is not set or empty, skipping extensions installation.' fi } # Run the extension installation in the background install_extensions & # --- END: Install Extensions after SillyTavern startup --- # Keep-alive loop while kill -0 ${SERVER_PID} 2>/dev/null; do echo "Sending keep-alive request to ${HEALTH_CHECK_URL}" # Use eval here as well for the keep-alive command eval "${CURL_COMMAND} ${HEALTH_CHECK_URL}" > /dev/null || echo "Keep-alive request failed." echo "Keep-alive request sent. Sleeping for 30 minutes." sleep 1800 done & wait ${SERVER_PID}