Files
infiplot-web/.env.example
T
DESKTOP-I1T6TF3\Q b0b5630a25 feat(web): export interactive gallery + encrypted share file
Adds a "导出图集" action at the bottom-right of the play canvas that
snapshots the current session into localStorage and opens
/gallery#id=<id> in a new tab — the original play page keeps running
untouched. In parallel, sends the doc to /api/gallery-pack and
downloads the result as a binary .infiplot file the player can send
to a friend.

The snapshot pulls in:
  - Every visited scene's image + beat graph + recorded visit trail
  - All AI-prefetched alternate scenes (a new resolvedPrefetchesRef in
    PlayInner captures each prefetch as it resolves, so abandoned
    branches the engine already paid to generate are kept)
  - Character names + basePortraitUrl (voice base64 / styleReference
    are stripped — they aren't needed for replay)

/gallery is a no-network interactive replay:
  - Per-beat advance and per-choice navigation. Picked choices are
    highlighted; unpicked choices are clickable when an alternate was
    prefetched, greyed otherwise.
  - Stack-based navigation for stepping into branches with one-tap
    "返回主线" to collapse back to the main path.
  - Top-bar batch download for scene images (including unique
    AI-prefetched branch scenes, deduped against the main path) and
    character portraits. Fetched with a per-file AbortController + 20s
    timeout in a small concurrency pool, then clicked serially.
    Prevents one slow CDN response from stranding the busy button.
  - In-progress hint banner reminding the player to allow the
    browser's "multiple downloads" prompt.
  - F-key fullscreen with a top toolbar that auto-retracts after the
    initial glance and pops back down on cursor approach.
  - Per-scene dialogue panel (fa-clock-rotate-left, matching the
    in-game history affordance).
  - "导入分享文件" entry on the empty/error state — accepts a friend's
    .infiplot, posts to /api/gallery-unpack, renders the decrypted doc.

Share-file format (.infiplot):
  - AES-256-GCM via Web Crypto (portable to Cloudflare Workers).
  - Layout: 4-byte magic "IFPL" + 1-byte version + 12-byte nonce +
    ciphertext (includes 16-byte auth tag).
  - Key derived from GALLERY_SECRET via SHA-256.
  - GCM's auth tag gives tamper-detection for free; any flip in the
    ciphertext/nonce surfaces as "文件校验失败" — same error as wrong-key,
    so the distinction can't leak server config.
  - Stateless: server keeps no record of issued files.
  - GALLERY_SECRET unset → /api/gallery-pack returns 503, the play page
    silently skips the share-file download, local view still works.
    Rotating the secret invalidates every previously-issued file.

Retention: trimGalleryExports keeps only the 2 most recent localStorage
docs; older ones are evicted before each write so quota stays flat
regardless of how many times the player exports. Share files live on
the player's own disk — no retention concern.

Adds 'gallery_export' to the analytics event schema (scene_count only —
no free text).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 12:08:37 +08:00

138 lines
7.7 KiB
Bash

# =============================================================
# InfiPlot — AI 实时交互剧情游戏
# Recommended setup: Xiaomi MiMo Token Plan for TEXT / VISION / TTS
# (one API key covers all three) + Runware for IMAGE (FLUX.2 [klein]).
#
# TEXT / VISION default to any OpenAI-compatible endpoint, and can switch to
# native Anthropic or Google Gemini via TEXT_PROVIDER / VISION_PROVIDER.
# TTS uses Xiaomi MiMo's own voice design / clone protocol
# (not OpenAI-compatible; appends -voicedesign / -voiceclone).
#
# IMAGE supports Runware (its own task-array protocol), OpenAI (gpt-image),
# and Google Gemini (Nano Banana) via IMAGE_PROVIDER.
#
# *_PROVIDER (optional) selects the wire protocol; leave unset for the
# OpenAI-compatible default (image is auto-detected from the URL). Base URLs
# tolerate a missing or extra /v1 (or a trailing /chat/completions) — the
# engine normalizes them.
# =============================================================
# ---- 1. Text LLM · scene director ----------------------------------
# Any OpenAI-compatible endpoint works: OpenAI, Anthropic (via proxy),
# Gemini, OpenRouter, DeepSeek, OpenCode, MiMo, local Ollama, …
# Recommended starters:
# A. DeepSeek v4-flash direct (https://api.deepseek.com/v1) — pay-as-you-go,
# fastest first-token latency, very stable JSON output.
# B. OpenCode Go (https://opencode.ai/zen/go/v1) — $10/mo flat-rate bundle of
# 12 open-source models (DeepSeek v4-flash, Qwen, Kimi, GLM, MiMo, …).
# Cheaper at high volume, slower at the tail.
# C. MiMo v2.5 via Xiaomi Token Plan — bundles VISION + TTS in one tp- key.
TEXT_BASE_URL=https://api.deepseek.com/v1
TEXT_API_KEY=sk-xxx
TEXT_MODEL=deepseek-v4-flash
# TEXT_PROVIDER: openai_compatible (default) | anthropic | google
# anthropic → TEXT_BASE_URL=https://api.anthropic.com TEXT_MODEL=claude-sonnet-4-6
# google → TEXT_BASE_URL=https://generativelanguage.googleapis.com TEXT_MODEL=gemini-3.5-flash
# TEXT_PROVIDER=openai_compatible
# ---- 2. Image generator (renders the scene background) -------------
# Recommended: Runware + FLUX.2 [klein] 9B KV — distilled 4-step model,
# sub-second inference at ~$0.0008/image. Sign up at https://runware.ai
# AIR ids for FLUX.2 [klein] variants:
# runware:400@1 · 4B (smaller)
# runware:400@6 · 9B KV (recommended — fastest at 16:9)
IMAGE_BASE_URL=https://api.runware.ai/v1
IMAGE_API_KEY=runware-xxx
IMAGE_MODEL=runware:400@6
# IMAGE_PROVIDER: runware (auto-detected for runware.ai) | openai_compatible
# | openai | google
# openai → gpt-image, supports referenceImages (character/scene continuity).
# IMAGE_BASE_URL=https://api.openai.com IMAGE_MODEL=gpt-image-1
# google → Gemini "Nano Banana" (Imagen is EOL 2026-06-24, do not use it).
# IMAGE_BASE_URL=https://generativelanguage.googleapis.com
# IMAGE_MODEL=gemini-2.5-flash-image
# NOTE: openai/google return raw bytes → inlined as a data: URI for the session
# (heavier per-call transport than Runware's UUID re-reference loop). Runware
# stays fastest + cheapest for the scene-by-scene flow.
# IMAGE_PROVIDER=runware
# ---- 3. Vision model · multimodal click interpretation -------------
# Recommended: MiMo V2.5 — multimodal, accepts image_url content parts.
VISION_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
VISION_API_KEY=tp-xxx
VISION_MODEL=mimo-v2.5
# VISION_PROVIDER: openai_compatible (default) | anthropic | google
# anthropic → VISION_BASE_URL=https://api.anthropic.com VISION_MODEL=claude-sonnet-4-6
# google → VISION_BASE_URL=https://generativelanguage.googleapis.com VISION_MODEL=gemini-3.5-flash
# VISION_PROVIDER=openai_compatible
# ---- 4. TTS · Xiaomi MiMo (optional — leave blank to disable) ------
# Per-character voice design → clone, with per-line delivery direction.
# Voice identity = the reference audio kept in the session (no server expiry).
# The adapter appends -voicedesign / -voiceclone to TTS_SPEECH_MODEL.
TTS_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
TTS_API_KEY=tp-xxx
TTS_SPEECH_MODEL=mimo-v2.5-tts
# ---- 5. MOCK_IMAGE — skip image generation (cheap TTS testing) -----
# true → return a placeholder image instead of calling the image model.
# Text/story/voice still run normally. Great for iterating on TTS.
MOCK_IMAGE=false
# ---- 5b. Image proxy (Cloudflare Worker, OPTIONAL) -----------------
# Leave NEXT_PUBLIC_IMAGE_PROXY_URL blank (the default) and the browser
# fetches images directly from the provider — exactly as the app worked
# before this proxy existed. The ALLOWED_HOSTS value below is inert until
# a proxy URL is set, so you're completely unaffected; skip this section.
#
# Why you might want it: Chrome's direct fetch of im.runware.ai is unreliable
# on some networks (ERR_QUIC_PROTOCOL_ERROR mid-stream → partial bytes →
# <img> paints progressively top-to-bottom). Routing the fetch through a tiny
# Cloudflare Worker re-fetches server-to-server (no QUIC fragility) and serves
# over HTTP/2 — atomic paint, plus edge caching + CORS.
#
# Deploy your own in ~1 min (one-click "Deploy to Cloudflare" button):
# https://github.com/zonghaoyuan/infiplot-image-proxy
# Then paste the workers.dev URL it prints below. NEXT_PUBLIC_ vars are
# inlined at BUILD time — set them in Vercel/Cloudflare project settings.
NEXT_PUBLIC_IMAGE_PROXY_URL=
# Hostnames the proxy is allowed to fetch (comma-separated). Default covers
# Runware's CDN. If your IMAGE_BASE_URL points at another provider, add that
# provider's image host here so its URLs take the proxy path too. Anything
# not listed stays on the direct fetch. Only matters when the URL above is set.
NEXT_PUBLIC_IMAGE_PROXY_ALLOWED_HOSTS=im.runware.ai
# ---- 6. Analytics · Umami (optional — leave blank to disable) ------
# Privacy-friendly, cookieless page-view stats — no Cookie consent banner.
# Cloud: sign up at https://cloud.umami.is, add your site, copy its ID into
# NEXT_PUBLIC_UMAMI_WEBSITE_ID and use the cloud script URL:
# NEXT_PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js
# Self-host later: point SRC at your own instance — the integration is identical
# (no code change), e.g. NEXT_PUBLIC_UMAMI_SRC=https://stats.example.com/script.js
# Both blank → no script is injected (zero tracking; every track() call no-ops).
# Beyond page views the app emits content-free custom events (game start, scene
# reached, choice picked, ...) — only enums/counts/booleans, never your prompts,
# uploaded images or any per-user ID. The visitor's Do-Not-Track is honoured.
# NEXT_PUBLIC_ vars are inlined at BUILD time, so set them in the build env
# (Vercel project settings).
NEXT_PUBLIC_UMAMI_SRC=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
# Optional hostname allowlist — defense-in-depth on top of the blank-to-disable
# gate above. The tracker fires only when window.location.hostname EXACTLY
# matches an entry, so a fork that copied these vars stays silent on its own
# domain. Comma-separated, exact match: apex ≠ www (list both), no wildcards.
# Blank → track on all hosts. e.g. infiplot.com,www.infiplot.com
NEXT_PUBLIC_UMAMI_DOMAINS=
# ---- 7. Gallery share files (optional — leave blank to disable) ----
# Server-side secret used to AES-256-GCM encrypt a played session into a
# binary `.infiplot` share file the player can send to a friend. Friends drop
# the file into /gallery; the server decrypts and renders the same interactive
# replay. GCM's built-in auth tag also gives tamper-detection for free.
# Blank → "导出分享文件" is hidden, only the same-browser localStorage flow
# remains. Set to any high-entropy string ≥ 32 chars (e.g. `openssl rand -hex 32`).
# WARNING: rotating this secret invalidates every share file ever issued
# (decryption will fail with "文件校验失败"). Only change when you're OK with that.
GALLERY_SECRET=