配置文件
-
Dockerfile
FROM node:20-alpine ARG APP_HOME=/home/node/app ENV USER=node ENV VENV_PATH=/opt/venv ENV PATH="$VENV_PATH/bin:$PATH" WORKDIR ${APP_HOME} RUN apk add --no-cache \ tini \ git \ python3 \ bash \ dos2unix \ findutils \ tar \ gcompat RUN python3 -m venv $VENV_PATH \ && pip install --no-cache-dir --upgrade pip \ && pip install --no-cache-dir huggingface_hub ENV NODE_ENV=production ENV USERNAME="admin" ENV PASSWORD="password" ENV FORCE_COLOR=1 ENV GITHUB_NAME=sillytavern ENV GITHUB_REPO=sillytavern RUN git clone --depth=1 https://github.com/${GITHUB_NAME}/${GITHUB_REPO}.git . \ && npm install --no-audit --no-fund --loglevel=error \ && npm cache clean --force COPY config.yaml ./ COPY entrypoint.sh ./ RUN dos2unix entrypoint.sh && chmod +x entrypoint.sh RUN mkdir -p config data plugins logs public/scripts/extensions/third-party \ && mv config.yaml ./config/ \ && ln -s ./config/config.yaml config.yaml \ && chown -R ${USER}:${USER} ${APP_HOME} ${VENV_PATH} USER ${USER} EXPOSE 7860 ENTRYPOINT ["tini", "--"] CMD ["./entrypoint.sh"]
-
entrypoint.sh
#!/usr/bin/env bash set -euo pipefail set -a APP_BASE_DIR="/home/node/app" USERNAME="${USERNAME:-admin}" PASSWORD="${PASSWORD:-password}" HF_TOKEN="${HF_TOKEN:-}" DATASET_ID="${DATASET_ID:-}" KEEP_BACKUPS="${KEEP_BACKUPS:-10}" SYNC_INTERVAL="${SYNC_INTERVAL:-3600}" DIRS_TO_BACKUP=( "data" "config" "plugins" "public/scripts/extensions/third-party" ) log() { echo "[$(date +'%F %T')] $*" } init_backup() { if [[ -n "${DATASET_ID:-}" ]]; then log "📁 使用提供的 DATASET_ID=${DATASET_ID}" return 0 fi if [[ -z "${HF_TOKEN:-}" ]]; then log "⚠️ 未设置 HF_TOKEN,跳过同步。" return 1 fi USER_ID=$(python3 <<'PY' import os, sys from huggingface_hub import HfApi try: print(HfApi(token=os.getenv("HF_TOKEN")).whoami().get("name")) except Exception: sys.exit(1) PY ) if [[ -z "$USER_ID" ]]; then log "❌ 无法从 HF_TOKEN 获取用户名,跳过同步。" return 1 fi DATASET_ID="${USER_ID}/data" log "✅ 默认 DATASET_ID 已设置为 ${DATASET_ID}" } prep_repo() { log "🔍 准备 HuggingFace 仓库..." python3 <<'PY' import os from huggingface_hub import HfApi from huggingface_hub.utils import HfHubHTTPError api = HfApi(token=os.getenv("HF_TOKEN")) repo_id = os.getenv("DATASET_ID") try: api.repo_info(repo_id=repo_id, repo_type="dataset") except HfHubHTTPError as e: if e.response.status_code == 404: print(f"仓库 '{repo_id}' 未找到。将创建为私有仓库。") api.create_repo(repo_id=repo_id, repo_type="dataset", private=True) else: raise PY log "✅ 仓库已准备就绪。" } restore_latest() { log "🔄 检查要恢复的备份..." python3 <<'PY' import os, sys, tarfile, tempfile from huggingface_hub import HfApi api = HfApi(token=os.getenv("HF_TOKEN")) repo_id = os.getenv("DATASET_ID") files = api.list_repo_files(repo_id=repo_id, repo_type="dataset") backups = sorted([f for f in files if f.endswith(".tar.gz")]) if not backups: print("未找到远程备份。跳过恢复。") sys.exit(0) latest_backup = backups[-1] print(f"找到最新备份: {latest_backup}。正在恢复...") with tempfile.TemporaryDirectory() as temp_dir: path = api.hf_hub_download( repo_id=repo_id, filename=latest_backup, repo_type="dataset", local_dir=temp_dir ) with tarfile.open(path, "r:gz") as t: t.extractall(os.getenv("APP_BASE_DIR")) PY log "✅ 恢复检查完成。" } do_backup() { local valid_dirs=() for dir in "${DIRS_TO_BACKUP[@]}"; do if [[ -d "${APP_BASE_DIR}/${dir}" ]]; then valid_dirs+=("$dir") fi done if [[ ${#valid_dirs[@]} -eq 0 ]]; then log "⚠️ 没有有效的目录可供备份,跳过此周期。" return fi log "⬆️ 开始备份过程..." local ts; ts=$(date +%Y%m%d_%H%M%S) local fname="sillytavern_backup_${ts}.tar.gz" local tmp_file; tmp_file=$(mktemp) tar -czf "$tmp_file" -C "$APP_BASE_DIR" "${valid_dirs[@]}" python3 <<PY import os from huggingface_hub import HfApi api = HfApi(token=os.getenv("HF_TOKEN")) repo_id = os.getenv("DATASET_ID") print(f"正在上传 {os.path.basename("$fname")}...") api.upload_file( path_or_fileobj="$tmp_file", path_in_repo="$fname", repo_id=repo_id, repo_type="dataset" ) print("正在清理旧备份...") files = api.list_repo_files(repo_id=repo_id, repo_type="dataset") backups = sorted([f for f in files if f.endswith(".tar.gz")]) keep = int(os.getenv("KEEP_BACKUPS", "10")) if len(backups) > keep: for old_backup in backups[:-keep]: print(f"正在删除旧备份: {old_backup}") api.delete_file(path_in_repo=old_backup, repo_id=repo_id, repo_type="dataset") api.super_squash_history(repo_id=repo_id, repo_type="dataset") PY rm -f "$tmp_file" log "✅ 备份周期完成。" } sync_loop() { while true; do do_backup log "⏳ 下次同步将在 ${SYNC_INTERVAL} 秒后进行。" sleep "${SYNC_INTERVAL}" done } main() { cd "$APP_BASE_DIR" || { log "❌ 致命错误: 无法访问 APP_BASE_DIR 位于 $APP_BASE_DIR"; exit 1; } trap 'kill 0' SIGTERM SIGINT log "🚀 sillytavern 启动器正在启动..." if [[ -f "config/config.yaml" ]]; then sed -i -e "s/username: .*/username: \"${USERNAME}\"/" \ -e "s/password: .*/password: \"${PASSWORD}\"/" "config/config.yaml" fi if init_backup; then log "🔗 已为仓库启用同步服务: $DATASET_ID" prep_repo restore_latest sync_loop & fi log "🟢 正在启动 sillytavern 服务器..." exec node server.js --listen "$@" } main "$@"
-
config.yaml
dataRoot: ./data listen: true listenAddress: ipv4: 0.0.0.0 ipv6: '[::]' protocol: ipv4: true ipv6: false dnsPreferIPv6: false browserLaunch: enabled: true browser: 'default' hostname: 'auto' port: -1 avoidLocalhost: false port: 7860 ssl: enabled: false certPath: "./certs/cert.pem" keyPath: "./certs/privkey.pem" whitelistMode: false enableForwardedWhitelist: true whitelist: - ::1 - 127.0.0.1 whitelistDockerHosts: true basicAuthMode: true basicAuthUser: username: "user" 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 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: false skipContentCheck: false whitelistImportDomains: - localhost - cdn.discordapp.com - files.catbox.moe - raw.githubusercontent.com - char-archive.evulid.cc requestOverrides: [] extensions: enabled: true autoUpdate: true 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 extendedTTL: false gemini: apiVersion: 'v1beta' enableServerPlugins: false enableServerPluginsAutoUpdate: true
环境变量
变量名 | 示例值 | 描述 |
---|---|---|
USERNAME | user | 必填,登录用户名 |
PASSWORD | password | 必填,登录密码 |
HF_TOKEN | hf_**** | 必填,HuggingFace令牌 |
SYNC_INTERVAL | 36000 | 选填,同步间隔,单位:秒 |