* Enable AWS-hosted text models without exposing ambient credentials
Rebase the Bedrock provider onto current main and close the review-requested security, credential lifecycle, usage attribution, configuration, and metadata gaps.
Constraint: Bedrock may use ambient AWS credentials only when explicitly enabled by the server operator
Rejected: Trust client-supplied provider types | permits built-in keyless IDs to reach Bedrock credentials
Confidence: high
Scope-risk: moderate
Directive: Keep provider ID/type validation at both request resolution and model construction boundaries
Tested: targeted Bedrock/resolver/config/usage tests; TypeScript; ESLint; i18n alignment; production build
Not-tested: Full suite has 27 failures in unchanged quiz/runtime and runtime/chat-storage tests
* Give Bedrock a recognizable provider identity
Use the official AWS architecture service icon so Bedrock no longer falls back to the generic provider cube in settings and model selection.
Constraint: Preserve the AWS-provided artwork without redesigning the service mark
Confidence: high
Scope-risk: narrow
Tested: SVG XML validation; provider unit test; TypeScript; ESLint; production build
---------
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
* feat(persistence): one-command server-backed stack — embedded API + compose postgres profile
docker compose --profile server-persistence up --build runs exactly two
containers: the app (persistence HTTP server embedded at /api/persistence,
enabled only when DATABASE_URL is set — unset keeps today's browser-only
behavior byte-for-byte) and PostgreSQL 16. Client bootstrap configures both
storage seams against the same-origin route when NEXT_PUBLIC_PERSISTENCE=1,
riding the existing lazy migrations so prior browser data reaches the
server per-course on first open. Dev auth (bearer token + client-asserted
learner header) lives in one overridable module and is loudly documented
as development-grade. Verified end-to-end against the real stack:
document PUT/GET and runtime session create land in Postgres; bad tokens 401.
Co-authored-by: Codex <codex@openai.com>
* fix(persistence): review round — structural bootstrap ordering, retryable init, honest failure surfaces
The bootstrap side-effect import moves into the two storage seam entry
modules, so any module graph that can resolve a store necessarily
evaluates it first — no client-entry ordering luck, and the two configure
calls are all-or-nothing. A failed server init clears its cached promise
(next request retries instead of permanent 500s). listStages rethrows
backend failures and the dashboard surfaces a persistence-unavailable
error instead of rendering an empty list that reads as data loss. Docs
state the build-time nature of NEXT_PUBLIC_PERSISTENCE, the real token
boundary (any visitor can extract it and impersonate any learner —
localhost/trusted-network only), and the correct postgres password
rotation. Dev-token compare is timing-safe; the handler singleton lives
on globalThis so HMR cannot leak pools.
Co-authored-by: Codex <codex@openai.com>
* fix(persistence): review response — no-confidentiality wording + adapter round-trip coverage
The token docs now state plainly that the NEXT_PUBLIC token is compiled
into the public bundle and provides no confidentiality or user isolation
(its only role is keeping unrelated scanners off a trusted network). The
Fetch/Node adapter — the route's most bug-prone code — gains a round-trip
integration test: PUT body streaming, 201 with multi-value headers, 204
empty body, and encoded path segments reaching the handler un-decoded.
---------
Co-authored-by: Codex <codex@openai.com>
* feat(video-export): service-backed MP4 render + in-app one-click export (#866)
Adds the last mile of classroom video export: turning the self-contained
Hyperframes project ZIP (#865) into an MP4 via an isolated render service,
one-click in-app.
- render-service/: standalone Node 22 + Chromium + FFmpeg container wrapping
@hyperframes/producer's library API. Async job model (POST /render -> 202
jobId, GET poll, GET download, DELETE cancel). Swappable JobStore /
ArtifactStore seams (in-memory + local-disk now; Redis/S3 + presigned-302
download later) so it scales horizontally without changing the HTTP contract.
Concurrency + per-user guards are config knobs.
- App integration: thin Next proxy routes under app/api/export-video/* (forward
only, no rendering) + capability probe. use-render-video.ts uploads the ZIP,
polls via runPolledTask, downloads the MP4; shared buildExportZip prefix with
the existing ZIP path. Export menu gains resolution/fps/quality selectors and
a progress bar; degrades to ZIP download when RENDER_SERVICE_URL is unset.
- docker-compose: render-service under an opt-in "video-export" profile.
- Entry is main.ts (not server.ts): the producer auto-starts its own server on
:9847 when the process entry path ends with /src/server.ts.
Verified end-to-end in the container: rendered a real 640s (10.7 min) classroom
ZIP to a valid H.264 720p + AAC MP4 (duration matches source) in ~9.6 min
(~0.9x realtime, 4-worker frame capture). Degrade path, queued-cancel + cleanup,
and per-user 429 guard all exercised. pnpm check / lint / tsc / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(video-export): global render progress store, percent+ETA UI, ring on export button (#866)
Addresses two UX issues found while driving the in-app MP4 export:
1. Progress display was raw and unfriendly (showed producer's English stage
strings like "Capturing frame 5130/19220") and had no time estimate. Now the
menu shows only "<percent>% · about <remaining> left". ETA is computed from a
recent-speed estimate (percent-per-ms over the last sample), EMA-smoothed —
which tracks the render's non-uniform pace (prep -> frame capture with a
4->1 worker drop -> encode) far better than a whole-run average, and never
shows a stale/rising ETA.
2. Switching scenes mid-render unmounted the export menu and lost the progress
(and reset the local "already rendering" ref, allowing a duplicate submit).
The whole render lifecycle now lives in a global store
(lib/store/video-render.ts), so progress survives menu close / scene switch
and duplicate submits are guarded by status. A persistent CircularProgress
ring on the export button shows live progress whether or not the menu is open.
Also fixes the progress scale: the producer reports progress as 0..100, but our
HTTP contract (and success path) is 0..1 — the service now normalizes it, so the
client no longer showed "2000%".
- lib/store/video-render.ts: new global store owning submit->poll->download,
recent-speed ETA, duplicate-submit guard.
- lib/video-export-app/use-render-video.ts: thin facade over the store.
- components/ui/circular-progress.tsx: lightweight SVG progress ring.
- components/stage/{header-controls,video-export-menu}.tsx: ring on the export
button; menu shows percent + ETA, subscribes to the store.
- render-service/src/render-manager.ts: normalize producer progress 0..100 -> 0..1.
- i18n: percent/ETA strings across all 8 locales (drops the stage-based string).
- render-service/package-lock.json: complete integrity hashes (reproducible npm ci).
Verified: ETA logic checked against the real segmented render curve (worker drop
raises ETA, encode speedup drives it to ~0); progress scale fix confirmed live
against the container (0.2 -> 20%). tsc / lint / prettier / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): persist render options in the store, not the menu component
Selecting 720p/24fps/draft, switching scenes, and reopening the export menu
showed the defaults again (1080p/30/standard). The selections lived in the
VideoExportMenu component's local state, which reset when the menu unmounted on
a scene switch — the running render still used the chosen options, but the UI
misrepresented them.
Move resolution/fps/quality into the global video-render store (with a
setOptions action). The menu now reads/writes the store, so selections survive
menu close / scene switch, and while a render runs the selectors reflect the
options that render is actually using. startRender() reads options from the
store instead of taking them as an argument.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): deployment correctness + resource/isolation controls (PR #937 review)
Addresses the blocking findings from wyuc's review. Output fidelity was fine;
these harden deployment and production resource/isolation boundaries.
#1 Compose advertised MP4 but couldn't render in prod:
- Capability now probes the service's /health (checkRenderServiceHealth), so a
configured-but-absent service reports disabled and the UI degrades to ZIP
instead of 502-ing.
- RENDER_SERVICE_URL is operator-supplied trusted config, so the proxy no longer
runs it through the SSRF guard — the one-command `docker compose --profile
video-export up` now works without globally weakening SSRF via
ALLOW_LOCAL_NETWORKS. resolveRenderServiceUrl() is now synchronous.
- Client degrades to ZIP on any failed submit (not only 501).
#2 Unbounded upload/queue (ZIP-bomb / DoS):
- unzip.ts bounds the archive via fflate's filter BEFORE decompression: entry
count, per-entry and total expanded size, and compression ratio.
- Proxy rejects oversized uploads (413) by Content-Length before forwarding.
- RenderManager enforces a global queue-depth cap (RENDER_MAX_QUEUE).
- All limits are env-tunable knobs in config.ts.
#3 Per-user guard was ineffective + admission ran after extraction:
- Identity is derived server-side (client IP) and forwarded as x-openmaic-client;
the service ignores any client-supplied userId, and the proxy strips it.
- Admission is split into reserve()/submit()/release(): the slot is reserved
BEFORE extraction, so a rejected caller never triggers a decompression.
Additional risks:
- Per-job wall-clock watchdog (RENDER_JOB_DEADLINE_MS) aborts + fails a hung
render so it can't hold a slot/scratch forever.
- Download proxy bounds only the time-to-headers, not the body stream, so large
MP4s over slow links no longer truncate.
- Client cancels the server job (DELETE) when a started render fails/times out.
- Compose puts render-service on an internal:true network (no host/internet
route), sandboxing the Chromium that runs the uploaded HTML; the export ZIP is
self-contained so no outbound is needed. README documents the standalone caveat.
Not closing #866: the smoke/golden-render CI acceptance criterion remains a
follow-up (see PR description).
Verified in-container: legal render 202; ZIP-bomb (entry-count + compression-
ratio) rejected 400 before any decompression; per-identity guard 429 with a
spoofed multipart userId ignored; reserve-before-extract leaves no scratch dir
on rejection; watchdog aborts an overrunning job and frees the slot. tsc / lint /
prettier / i18n pass; render-service tsc passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): real audio durations + burned-in subtitles (PR #937 review)
Two export-fidelity issues found in wyuc's deeper E2E:
A. Narration was scheduled from estimated durations, cutting audio off mid-
sentence and advancing the timeline early. The scheduler trusted
AudioFileRecord.duration (recorded only since #861), so the many existing
classrooms without it fell back to text-length estimates — measured 4.35s
average / 10.23s max underestimate across 47 clips. timeline-deps now probes
the real duration from each narration blob via an off-document <audio>
(symmetric to the existing video probe), preferring it over the stored
duration, then the estimate only when no audio asset exists. Everything
downstream (narration starts, scene/total duration, subtitle cues) re-derives
from the corrected value in the pure compiler — no compiler change needed.
B. The final MP4 had no subtitles (only H.264+AAC), and the ZIP's SRT/VTT used
the same estimated boundaries. The emitter now renders a burned-in subtitle
overlay: one caption box + a hidden div per cue, revealed/hidden by the paused
GSAP timeline at each cue's start/end (corrected timings from A), so Chromium's
frame capture bakes them in. The producer has no subtitle track of its own, so
burn-in is the v1 approach.
Verified: emitter unit tests + snapshot updated (subtitle overlay + toggle
statements, escaped text, hidden-by-default); 82 video-export tests pass incl.
the determinism red-line proxy. Rendered a synthetic subtitle project through the
container and confirmed by pixel analysis that captions appear only within their
cue window (2429 near-white px in the caption band at t=1.5s vs 0 at t=0.05s).
tsc / lint / prettier / i18n pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): subtitle layout + upload/admission hardening (PR #937 review)
Address the three P1 blockers plus actionable P2s from the 6a585c29 review.
P1:
- emit-hyperframes: stack every subtitle cue in one grid cell and toggle
display:none/inline-block, so inactive cues leave the flow instead of
pushing the active cue up into the slide title. Adds a multi-cue
regression test the single-cue snapshot couldn't catch.
- render route + service: cap the upload by actual bytes (capBodyStream),
not the spoofable Content-Length; the app now streams the multipart body
through instead of buffering it via formData(). maxUploadBytes is now read.
- render-service: move makeProjectDir() inside the release()-guarded block
so an ENOENT/ENOSPC no longer permanently leaks the admission slot; mkdir
the scratch root at startup for the standalone path.
P2:
- config: allow RENDER_MAX_JOBS_PER_USER=0 to disable the per-identity guard.
- timeline-deps: per-probe timeout + bounded concurrency so a stuck audio
blob can't wedge export in "compiling" forever.
- render route: only trust x-forwarded-for/x-real-ip under
TRUST_PROXY_HEADERS=true; otherwise all callers share one "direct" bucket.
- render-service: add vitest tests (unzip limits/traversal, reservation
arithmetic, body cap, config zero-disable) and a dedicated CI job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): sync render-service lockfile so `npm ci` passes in CI
The vitest devDependency's transitive esbuild@0.28.1 (and its platform
optionals) were missing from package-lock.json, so the new CI job's
`npm ci` failed with EUSAGE. Regenerated the lockfile from a clean install.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): dedupe esbuild so render-service `npm ci` installs on linux
@hyperframes/core pins esbuild@0.25.12 exactly, hoisting it to the top and
forcing vite@8 (via vitest) to keep a nested esbuild@0.28.1 copy. npm fails to
flag that nested copy's platform-specific optionals as optional, so `npm ci`
tried to install @esbuild/aix-ppc64 on linux and died with EBADPLATFORM.
Add an `esbuild: 0.28.1` override so a single copy is shared (satisfies tsx
~0.28 and vite ^0.27||^0.28); esbuild is build-time only, so pinning the
producer's bundled build tool is runtime-inert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): resource/isolation hardening (PR #937 round-2 review)
Address the round-2 P1/P2/P3 findings (P1#1 lockfile EBADPLATFORM was already
fixed by the earlier esbuild-dedupe commit; CI Render Service job is green).
P1:
- Admission before buffering (#2): the render service now reserve()s the slot
from the header identity BEFORE parsing/buffering the multipart, so concurrent
near-cap uploads are bounded by the queue depth, not just each body by the cap.
- Chromium egress lockdown (#3): the producer exposes no browser-arg hook and the
render shares the internal network with the app, so a container entrypoint
installs an iptables egress lockdown (drop all outbound except loopback +
established replies) then drops privileges. Needs CAP_NET_ADMIN (added in
compose); graceful warn-and-continue if unavailable. The self-contained ZIP
needs no outbound.
- Default one-render bottleneck (#4): with no trusted proxy every caller is
"direct", so RENDER_MAX_JOBS_PER_USER=1 throttled the whole deployment. Default
compose now sets it to 0 and relies on concurrency + global queue caps.
- Non-blocking bounded extraction (#5): unzipSync -> fflate async unzip (worker,
off the event loop), keeping the pre-decompression filter; default expanded
ceiling 1GB -> 512MB; a semaphore caps concurrent extractions; compose adds a
container mem_limit.
P2:
- Raise the app submit timeout/maxDuration (300MB upload can't finish in 60s).
- video-render store: only degrade to ZIP when the service is genuinely
unavailable (501/unreachable); surface real 429/413/5xx instead of an
unsolicited download.
- useExportVideo dedupe guard moved to module scope so it survives the menu
unmounting (no second concurrent ZIP pipeline).
- .env.example: RENDER_SERVICE_URL bypasses SSRF; drop the ALLOW_LOCAL_NETWORKS note.
P3:
- Deadline overruns are marked failed (not cancelled).
- submit() decrements the identity slot if jobs.create throws (no leak).
- CI sets PUPPETEER_SKIP_DOWNLOAD; unzip tests use tiny fixtures + low env limits.
Tests: render-service now 22 tests (unzip limits/traversal, admission incl.
create-leak, body cap, semaphore, config); app video-export suite unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(video-export): buffer under the extraction permit + fail-closed egress (PR #937 round-3)
Address the two remaining round-3 P1 boundary blockers.
P1#1 — buffering was outside the gate: `bounded.formData()` materialized the
whole uploaded file into memory BEFORE `extractionGate.run()`, so up to
RENDER_MAX_QUEUE (20) admitted bodies could each buffer ~300MB (≈6GB vs the 4g
mem_limit) before the 2-permit gate. Move the entire RAM-heavy section —
formData buffering, file read, and unzip — INSIDE the permit; the queue
reservation still runs first (a rejected caller consumes nothing). Requests
beyond the permit wait with their body unconsumed (socket backpressure), so at
most maxConcurrentExtractions bodies are buffered at once. Refactored main.ts
into a testable `createApp(deps)` factory and added an integration test proving
peak concurrency in the buffering+extraction section never exceeds the permits.
P1#2 — egress lockdown failed open: the entrypoint warned and started normally
if iptables setup failed, so /health stayed green while Chromium could reach the
app. With RENDER_EGRESS_LOCKDOWN=true (default) it now FAILS CLOSED — exits
non-zero if not root, iptables is missing, or the rules don't apply. Operators
accepting an unisolated setup opt out with RENDER_EGRESS_LOCKDOWN=false. Added
scripts/egress-smoke.sh to assert the boundary (lockdown active, loopback works,
new outbound blocked).
Verified: image builds; container boots as `render` with lockdown active and
serves /health; fail-closed exits 1 without CAP_NET_ADMIN; egress smoke passes
(outbound blocked); 23/23 render-service tests + tsc; app tsc + root prettier clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch the OpenMAIC root and the in-house @maic/* SDK packages
(@maic/dsl, @maic/importer, @maic/renderer) from AGPL-3.0 to the MIT
License. Updates LICENSE files, package.json license fields, README
badges and license sections (EN/ZH), CONTRIBUTING, and renderer FONTS
note.
Third-party vendored packages are left untouched: packages/mathml2omml
remains LGPL-3.0, packages/pptxgenjs remains MIT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README / README-zh advertised 4 UI languages and labelled the i18n
directory "(zh-CN, en-US)", but the app ships 7 locales:
zh-CN, zh-TW, en-US, ja-JP, ru-RU, ar-SA, pt-BR.
- CONTRIBUTING's project structure listed a top-level `messages/`
directory for translations that does not exist; translations live
in `lib/i18n/locales/`.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
- drop "voice profiles live in your browser, so the serverless setup
model is preserved" / "not synced to the server" — overclaims
privacy: clone reference audio is sent to the VoxCPM backend on
every synthesis (that's how cloning works).
- correct UI label: voice manager → "VoxCPM Voices" / "VoxCPM 音色"
(matches voxcpmVoicesTitle).
- drop "voice cloning requires a backend that supports reference
audio" tail — all three shipped backends do, and when
voxCPMBackendSupportsReferenceAudio returns false, voice-resolver
filters clone profiles out of the picker (no silent fallback).
- replace stacked em-dashes in list items with colons; trim AI-tone
intro to one short line.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Optional: VoxCPM2 (Self-Hosted TTS with Voice Cloning) section
under Quick Start in both README.md and README-zh.md, mirroring the
MinerU optional-block style. Three-step structure: pick a backend
(vLLM-Omni / Python API / Nano-vLLM comparison table), configure in
Settings -> Text-to-Speech -> VoxCPM2 with a UI screenshot, and
manage voices (Auto / Prompt / Clone) with a UI screenshot.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(providers): add international base URL chips for GLM/Kimi/MiniMax
- Add optional alternateBaseUrls field to ProviderConfig
- Populate GLM, Kimi, MiniMax with China/International endpoints
- Settings UI renders chips below base URL input; clicking fills and auto-saves
- i18n: settings.baseUrlRegion.{china,international} in all 5 locales
- README / README-zh: list GLM and add quick-examples block
Closes#448
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: use glm-5.1 as the GLM default model example
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
Add MiniMax LLM models (M2–M2.7), TTS provider (speech-2.8/2.6/02/01),
image generation (image-01), and video generation (Hailuo 2.3/02,
T2V-01). Unify base URLs to api.minimaxi.com and add MiniMax-specific
base URL normalization for the Anthropic-compatible endpoint.
Co-authored-by: Hi-Jiajun <3138947285@qq.com>
* feat: add Grok (xAI) provider support for LLM, Image and Video generation
- Add Grok as LLM provider (OpenAI-compatible, 11 models including grok-4.20, grok-4, grok-3)
- Add Grok Image generation provider (grok-imagine-image, grok-imagine-image-pro)
- Add Grok Video generation provider with async polling (grok-imagine-video)
- Add env vars: GROK_*, IMAGE_GROK_*, VIDEO_GROK_*
- Add web search note: Grok search is via chat completions, not standalone API
- Update README/README-zh with Grok in supported providers
Closes#87
* fix: ensure new image/video providers are auto-added to persisted settings
Add ensureBuiltInImageProviders and ensureBuiltInVideoProviders functions
that run on every store rehydrate, mirroring the existing LLM provider
sync logic. Without this, Grok Image/Video providers would not appear
for users with existing localStorage settings.
* fix: add missing English translation for providerGrokVideo
* style: fix prettier formatting for grok video provider
* fix: remove unsupported grok multi-agent model and improve video error
Restructure the OpenClaw quickstart: install skill first, then pick mode.
Add meme-style nudge for OpenClaw to star the repo. Enlarge logo and
put "OpenClaw" in the TIP heading for visibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add [!TIP] callout block between Overview and Quick Start showing
hosted mode (3-step zero-setup) and self-hosted options
- Update OpenClaw Integration section to distinguish hosted vs self-hosted
- Add accessCode field to skill config examples in both READMEs
- SKILL.md: auto-detect accessCode from config, skip mode selection prompt
- hosted-mode.md: read access code from config file instead of chat input
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add OpenClaw badge to header badge row and nav links
- Mention OpenClaw integration in Overview paragraph and Highlights
- Move OpenClaw section from standalone h2 into Features as h3,
between Multi-Agent Interaction and Export
- Collapse config/details into <details> for cleaner reading
- Add portrait demo GIF placeholder for Feishu walkthrough
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update to correct `openclaw plugins install` command and
`~/.openclaw/openclaw.json` config path with proper structure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document how to install, configure, and use the OpenClaw plugin
for managing and generating classrooms via AI agents. Also add
openclaw/ to the project structure tree in both READMEs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove bundled MinerU Docker service and Dockerfile; link to official API and self-host docs instead
- Set MinerU requiresApiKey to false (self-hosted doesn't need it)
- Add test connection button to PDF settings panel with backend proxy to avoid CORS
- Add verify-pdf-provider API route for server-side connection testing
- Set Vercel API body size limit to 50MB for document uploads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace MIT license with AGPL-3.0 for compatibility with PPTist-derived
code. Add commercial licensing section, project structure diagram, and
key architecture pointers to both English and Chinese READMEs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add README.md (English) and README-zh.md (Chinese) with banner, GIF
demos, feature showcase, use cases, and quick start guide
- Add Dockerfile (multi-stage build), docker-compose.yml, .dockerignore
- Add .env.example with all supported environment variables
- Add MIT LICENSE file
- Move all README assets into assets/ directory
- Update i18n: new slogan and input placeholder text
- Add Vercel one-click deploy button and instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make all provider API keys optional in the one-click deploy link so
users are not forced to provide an OpenAI key.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>