# ───────────────────────────────────────────────────────────── # 这里的值是「默认值」。除 REDIS_URL / LLM_* / LLM_PROVIDER_SECRET / # WECHAT_AI_TOKEN / LINUXDO_ADMIN_IDS / COOKIE 与 URL 相关项之外, # 绝大多数都可以在 /admin → 设置 页直接改,覆盖存在 Redis, # 各节点 5 秒内生效,不必改这个文件再重启。 # 详见 docs/runtime-settings.md # ───────────────────────────────────────────────────────────── # Server(env-only:监听地址在 listen() 时固定) # 本地开发用 127.0.0.1;Docker / 公网部署用 0.0.0.0 WECHAT_AI_HOST=127.0.0.1 WECHAT_AI_PORT=8787 # 公网地址(OAuth 回调与文档用),Docker 请改成 https://你的域名 PUBLIC_BASE_URL=http://127.0.0.1:8787 # 跨域白名单(逗号分隔 Origin)。默认已含 PUBLIC_BASE_URL 的 origin # CORS_ORIGINS=https://your.domain.com # optional legacy/script token (OAuth is primary) WECHAT_AI_TOKEN=change-me-long-random COOKIE_SECURE=false SESSION_COOKIE_NAME=wa_session # Process L1 cache over Redis (session/user/prompt). Set false to disable. # REDIS_L1_CACHE=true # REDIS_L1_SESSION_MS=45000 # REDIS_L1_USER_MS=30000 # REDIS_L1_PROMPT_MS=60000 # 默认人设(每次开场都要读,改人设后最多滞后这个时间生效) # REDIS_L1_DEFAULT_PERSONA_MS=60000 # 广场列表快照(人设/表情包)。翻页、搜索、排序共用一份,免去每次 SMEMBERS+MGET 全量。 # 多节点下,别的节点发布的新内容最多滞后这个时间才出现在广场。 # REDIS_L1_SQUARE_MS=10000 # 管理台仪表盘统计快照(doctorSnapshot 会扫全量 bot/peer/persona) # REDIS_L1_DOCTOR_MS=15000 # 仪表盘的 assignments / messages / memories 三项每个 peer 约多读 4 个 key。 # peer 数超过这个上限就整体跳过,快照返回 deepStats=false(诊断里显示「未统计」 # 而不是 0)。设 0 表示永不统计。 # DOCTOR_DEEP_STATS_MAX_PEERS=5000 # 超管身份缓存(解析一次要扫全部用户;授权/删号会立即失效) # REDIS_L1_SUPERADMIN_MS=120000 # Remote Redis (required) — Upstash example (TLS = rediss://) # Console → your DB → Connect → copy "Redis URL" for ioredis / Node REDIS_URL=rediss://default:YOUR_UPSTASH_PASSWORD@YOUR_ENDPOINT.upstash.io:6379 # REDIS_CONNECT_TIMEOUT_MS=15000 # REDIS_TLS=true # 并发命令合并进同一次往返(远端 Redis 必开)。REDIS_AUTO_PIPELINE=false 可关闭 # REDIS_AUTO_PIPELINE=true # REDIS_KEEPALIVE_MS=10000 # 单命令重试次数。宁可快速失败让上层恢复,也不要盲目重试堆积 # REDIS_MAX_RETRIES=2 # 4=IPv4 / 6=IPv6 / 0=系统默认。不设时 Upstash 自动用 4(双栈 DNS 会多一跳) # REDIS_FAMILY=4 # 静态页(app/admin/docs 外壳)启动时预压缩 br+gzip,避免每次请求现压。 # STATIC_PRECOMPRESS=true # 出站超时(毫秒)。没有它,卡死的上游会一直占着一个 reply consumer # 和该 bot:peer 的串行链,直到 OpenAI SDK 默认的 600s × 3 次重试跑完。 # LLM_TIMEOUT_MS=120000 # CHATFLOW_HTTP_TIMEOUT_MS=15000 # Multi-node labels (optional) # WORKER_ID=node-01 # NODE_LABEL=cn-east-1a # NODE_REGION=cn-east # APP_VERSION=0.2.0 # OTA fleet update (file-diff + auto restart; default on) # OTA_ENABLED=true # OTA_ALLOW_INSTALL=true # OTA_STAGING_DIR=.wa-update-staging # Bot tokens: Redis key wa:bot:{id}:creds (no local token files) # Stickers: meta + image blob in Redis (wa:sticker:* / wa:sticker:{id}:blob) # STICKER_MAX_BYTES=2097152 # STICKER_SEND_ENABLED=true # MAX_STICKERS_PER_REPLY=2 # ── Inbound media (入站图片与语音) ── # WeChat's own speech-to-text. Default ON and deliberately independent of # VISION_ENABLED: the transcript arrives inside the inbound message, so using it # costs nothing and needs no model at all. Set false to have voice notes # answered with "didn't catch that" instead. (The audio itself is SILK/AMR, # which no OpenAI-compatible endpoint accepts, so there is no ASR path here.) # VOICE_TRANSCRIPT_ENABLED=true # # Image understanding. Master switch, default OFF — off means images are never # downloaded and the bot simply says it cannot see them. # VISION_ENABLED=false # # How the image reaches the conversation: # caption (default) — a vision endpoint describes the image, and only that # TEXT goes to the roleplay model. **The roleplay model needs no vision # support at all**, so this is the mode to use with deepseek and friends. # Bonus: the description is stored in history, so the model can still talk # about the image several turns later. # direct — hand the image to the roleplay model as content parts. Requires # that model to be vision-capable; it errors outright otherwise. # VISION_MODE=caption # # The vision endpoint. Admin-level creds, dialed directly like the platform LLM # (a captioner is not user-supplied, so it does not need the tools gateway). # Leave base/key empty to reuse LLM_BASE_URL / LLM_API_KEY — that works when the # provider hosts a vision model alongside the chat model. # VISION_MODEL is REQUIRED; without it every image is reported as unreadable. # VISION_BASE_URL= # VISION_API_KEY= # VISION_MODEL= # Caption length cap — a description, not an essay. # VISION_CAPTION_MAX_TOKENS=300 # Each image costs real tokens, so cap how many one message may spend. # VISION_MAX_IMAGES=2 # Per-attachment download cap (decrypted bytes; base64 adds ~1/3 on top). # INBOUND_MEDIA_MAX_BYTES=4194304 # ── Platform LLM (admin) — main site connects DIRECTLY ── LLM_BASE_URL=https://api.openai.com/v1 LLM_API_KEY=sk-... LLM_MODEL=gpt-4o-mini # ── Tools gateway (HF wechat-ai-tools) ───────────────── # User custom OpenAI-compatible APIs + web search egress ONLY via this service. # Main site never dials user base_url or search engines. # Local: cd huggingface/wechat-ai-tools && docker build -t wechat-ai-tools . && docker run -p 7860:7860 --env-file .env wechat-ai-tools # TOOLS_BASE_URL=http://127.0.0.1:7860 # TOOLS_API_KEY=change-me-shared-with-tools-service # TOOLS_TIMEOUT_MS=60000 # WEB_SEARCH_ENABLED=true # WEB_SEARCH_MAX_RESULTS=5 # Encrypt user-stored custom API keys (required when users add providers) # LLM_PROVIDER_SECRET=long-random-secret-at-least-16-chars # Chatflow: http nodes only hit tools host + this allowlist (comma hosts) # Exact hostnames, no wildcards — api.example.com does not cover sub.api.example.com. # Set to a single `*` to allow any PUBLIC host instead. `*` is not a blanket # bypass: loopback, RFC1918, link-local (169.254.169.254), CGNAT # (100.100.100.200), unique-local IPv6 and single-label/intranet names stay # blocked, and every redirect hop is re-checked. Graphs are authored by any # persona owner, so `*` hands every registered user a server-side fetch. # CHATFLOW_HTTP_ALLOWLIST= # CHATFLOW_MAX_STEPS=32 # CHATFLOW_MAX_NODES=40 # LINUX DO OAuth (https://connect.linux.do) LINUXDO_CLIENT_ID= LINUXDO_CLIENT_SECRET= # must match app registration callback, e.g. http://127.0.0.1:8787/api/v1/auth/callback LINUXDO_REDIRECT_URI=http://127.0.0.1:8787/api/v1/auth/callback # Comma-separated LINUX DO user ids and/or usernames who become admins LINUXDO_ADMIN_IDS=12345,your_username # Optional overrides: # LINUXDO_AUTHORIZE_URL=https://connect.linux.do/oauth2/authorize # LINUXDO_TOKEN_URL=https://connect.linux.do/oauth2/token # LINUXDO_USERINFO_URL=https://connect.linux.do/api/user # LINUXDO_SCOPE=openid profile # First successful signup becomes admin when LINUXDO_ADMIN_IDS is empty # FIRST_USER_IS_ADMIN=true # Local username+password auth (+ invite-only local register) # LOCAL_AUTH_ENABLED=true # PASSWORD_MIN_LENGTH=8 # INVITE_REQUIRED_FOR_LOCAL=true # INVITE_CODE_TTL_SEC=604800 # INVITE_CODE_LENGTH=10 # INVITE_MAX_PENDING_PER_USER=20 # Per user: max INVITE_QUOTA_MAX codes every INVITE_QUOTA_WINDOW_HOURS hours # INVITE_QUOTA_WINDOW_HOURS=24 # INVITE_QUOTA_MAX=3 # Product DEFAULT_PERSONA_SLUG=catgirl ALLOW_UNAPPROVED_USERS=false SHORT_HISTORY_LIMIT=20 MEMORY_EXTRACT_EVERY_N=8 # Memory retrieval (no embedding API): text overlap top-K when over FULL_INJECT_MAX # MEMORY_TOP_K=12 # MEMORY_FULL_INJECT_MAX=20 # MEMORY_MAX_ITEMS=100 # get_current_time tool (no external key). Set TIME_TOOL_ENABLED=false to disable. # TIME_TOOL_ENABLED=true # TIME_TOOL_TIMEZONE=Asia/Shanghai PEER_RATE_PER_MINUTE=20 # Human-like multi-bubble replies # MULTI_BUBBLE_JSON: primary model must return {"messages":["..."]} (and sticker objects when catalog is present) # REPLY_FILTER_ENABLED: optional second-pass AI that reformats primary reply into JSON (extra latency/cost; default off) # SPLIT_REPLY: if JSON parse fails, fallback to punctuation/paragraph split REPLY_FILTER_ENABLED=false MULTI_BUBBLE_JSON=true SPLIT_REPLY=true MAX_REPLY_CHUNKS=5 MAX_CHUNK_CHARS=72 # Delay between bubbles (ms) — higher = more human typing feel REPLY_DELAY_MS_PER_CHAR=90 REPLY_DELAY_MIN_MS=1400 REPLY_DELAY_MAX_MS=5500 REPLY_DELAY_FIRST_MIN_MS=900 REPLY_DELAY_FIRST_MAX_MS=2200 REPLY_DELAY_THINK_EXTRA_MS=400 # User-to-user chat via @{LINUX DO username} (bind code in /app first) # P2P_ENABLED=true # P2P_BIND_CODE_TTL_SEC=600 # P2P_REQUEST_TTL_SEC=300 # P2P_SESSION_IDLE_SEC=1800 # P2P_RELAY_MAX_CHARS=500 # P2P_MAX_REQUESTS_PER_DAY=20 # Proactive outreach (idle → LLM message). Global hard off by default. # Requires: PROACTIVE_ENABLED=true + bot switch + per-peer switch + prior context_token # PROACTIVE_ENABLED=false # PROACTIVE_IDLE_HOURS=12 # PROACTIVE_MIN_INTERVAL_HOURS=24 # PROACTIVE_MAX_PER_DAY=1 # PROACTIVE_QUIET_HOURS=0-8 # PROACTIVE_SCAN_INTERVAL_SEC=300 # PROACTIVE_MAX_PER_SCAN=10 # PROACTIVE_LOCK_TTL_SEC=180 # PROACTIVE_ATTEMPT_COOLDOWN_HOURS=1 # Admin broadcast (plain text to WeChat peers via /admin). Requires WORKER_ENABLED. # Interval between messages (ms). Lower = faster; raise if iLink rate-limits. # BROADCAST_INTERVAL_MS=200 # BROADCAST_MAX_TEXT=2000 # BROADCAST_HISTORY=100 # Web try-chat (persona preview in /app, no WeChat). Set TRY_CHAT_ENABLED=false to disable. # TRY_CHAT_ENABLED=true # TRY_CHAT_MAX_USER_MSGS_PER_DAY=40 # TRY_CHAT_MAX_USER_MSGS_PER_SESSION=20 # TRY_CHAT_SESSION_TTL_SEC=3600 # TRY_CHAT_MAX_HISTORY=40 # Persona fork from square (default on) # PERSONA_FORK_ENABLED=true # Worker (same process as API — one image / one container) WORKER_ENABLED=true # Cap concurrent iLink long-polls in this process (raise for more bots) MAX_BOTS_PER_WORKER=500 # Lease TTL for multi-replica of the same image (optional scale-out) LEASE_TTL_SEC=45 LEASE_RENEW_SEC=15 # Multi-node: overloaded workers release excess leases so idle nodes can claim. # Default is an even split; per-node load weights (0-500%, default 100) are set # at runtime in /admin -> 节点 -> 负载权重 and stored in Redis, not here. # REBALANCE_ENABLED=false freezes existing leases, so weights then only affect # newly claimed bots. # REBALANCE_ENABLED=true # REBALANCE_INTERVAL_SEC=60 # REBALANCE_SLACK=2 # REBALANCE_MAX_PER_TICK=50 # How long a node's load weight survives after its heartbeat stops, before the # fleet deletes it automatically. Must outlast a restart / OTA apply. # WORKER_WEIGHT_TTL_SEC=3600 # Pin WORKER_ID per node to keep its load weight across restarts — without it # every restart gets a fresh random id and starts back at the default 100%. # WORKER_ID=node-a # Concurrent LLM + WeChat send jobs (poll is decoupled from reply) REPLY_CONCURRENCY=16 INBOX_MAX_LEN=20000 # silent | fatal | error | warn | info | debug | trace (anything else → info). # Drives Fastify's logger, which emits one line per request plus framework # errors. Successful /health probes are skipped so the container healthcheck # does not bury everything else. LOG_LEVEL=info # Successful requests slower than this are logged at warn. # LOG_SLOW_REQUEST_MS=1000 # ── Admin live activity stream (SSE, /admin) ── # DATA_STREAM_ENABLED=true # Redis command sample rate (0–1) — every command would flood the stream # DATA_STREAM_REDIS_SAMPLE=0.08 # Process-local cap on stream events per second # DATA_STREAM_MAX_EPS=80 # Legacy sqlite path — read only by the db migrate CLI, not at runtime # WECHAT_AI_DB_PATH=data/wechat-ai.db # ── Multi-node (homogeneous replicas + Cloudflare Worker LB) ── # Every node shares the same REDIS_URL and PUBLIC_BASE_URL (main domain). # Unique per process (recommended on multi-node): node-01, node-02, … # WORKER_ID=node-01 # Optional ops labels shown in admin「节点」(not public URLs) # NODE_LABEL=cn-east-1a # NODE_REGION=cn-east # APP_VERSION=0.2.0 # Source node IPs live only in cloudflare-worker ORIGINS — see cloudflare-worker/README.md