-
参考 PixVerse world.pixverse.video/live/dress 的交互设计(分析见 docs/reference/pixverse-dress-live/NOTES.md),按 docs/todo/dev-plan-6.md 的任务表实现: - T40: 顶部"正在换装"胶囊 + 排队头像条 - T41: 换装卡片 hover/点击展开"发送"态,避免误触直接提交 - T42: 右侧"选衣服"网格改为贴视频底部的横条,"传衣服/传人"付费上传 入口合并进横条末尾的 DIY 卡片二选一弹窗,左栏撤掉多余 tab - T43: 修复移动端视频宽度驱动的 9:16 布局把"待播放"挤压到不可用的 问题,改回高度驱动 - T44: 上传进度条(伪进度+真实 XHR 进度混合) - T46: dance room 专属聊天(不复用 /api/chat 以免误触发生成任务, 独立限流/审核/按房间隔离存储) - T48: 同一访客同时只能有一条免费换装请求在排队,付费上传不受影响 新增/修改的后端集成测试覆盖 T46/T48 的核心行为;node --test *.test.js 142/142 通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4TVxY4MRoxDPwNhcK5Z34
-
/lib/video/:id proxied every clip live through H3 console's file endpoint, which is slow even for already-finished files (~35KB/s, confirmed 2026-09) — the 10s server-clock playlist kept advancing before a clip ever finished loading, so rooms looked dead, sound silently got re-muted on transient play() rejections, and a failed load just froze forever. - server.js: local disk cache (video-cache/) for H3-proxied clips, warmed ahead of time by /api/dance/live/state for the current + next-up clip; /lib/video/:id serves cache hits instantly with Range support - dance.html: only fall back to muted autoplay on a real NotAllowedError, not any play() rejection (e.g. AbortError from a mid-switch load); auto-skip to the next clip on a video load error instead of freezing; drop the now-playing garment-prompt text under the video - dance-channel.test.js: mock H3 server no longer conflates the new cache warm-up request with job-status polling Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHPGtozYFzo39AX2CdV5ZY
-
Extends the 跳舞直播间 (dance room) system to one independent live room per character, with an operator playback-management/blacklist layer (T39), a "我的作品" page for paid users (T38), a standalone 人衣图生成 admin tool, and a new admin-dance-library.html page for curating the per-character dance clip pool with AI QC. On top of that, dance clips now have an explicit published (上架/下架) status: passing AI QC (or a manual admin override to "correct") auto- publishes a clip, failing it auto-unpublishes it, and only published clips are eligible for a room's random-fill pool — unreviewed/rejected clips stay in the library but no longer autoplay for viewers. Existing already-QC'd "correct" clips are backfilled to published on first boot so the room doesn't go quiet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvkJsRcQ2qEuaFea31rBY8
-
Both are pluggable seams (dev-plan-4.md §1.2 items 1-2), same shape as T20's sendVerificationEmail: a fixed env var gates a real vendor call that isn't implemented yet (none has been picked), and the no-vendor path is what actually runs today. describeGarmentImage() — mode 2 has no admin around to hand-write garments.appearanceDescriptor the way admin_seed garments do, so it needs *something* non-empty to fill that field; today that's a fixed placeholder string, swapped for a real vision-model call once a vendor's picked. moderateGarmentImage() — the requirements doc flags image-content moderation as vendor-TBD but never actually lists it as its own task; this fills that gap. Every call writes a moderation_log row (new dressup-store.js export, M.9 schema) regardless of which branch runs, so the audit trail exists before a real vendor does. Default-permissive when unconfigured — T2's upload endpoint already enforces the real trust-boundary checks (magic-byte type check, 8MB cap, EXIF-stripped re-encode); this only covers the *content* check, which nothing here can perform without a vendor. Neither is unit-tested standalone (server.js doesn't export internal functions, matching sendVerificationEmail's existing precedent) — T27 integration-tests both through the actual upload endpoint. Also adds pricing.js to the tmpdir file-copy list in every test that spawns server.js in isolation — server.js now requires it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, and a debug GET /api/auth/me (T19's own acceptance criteria calls for it explicitly). Passwords hashed with node:crypto's scryptSync (salt stored alongside the hash, verified with timingSafeEqual) — still zero new dependencies. Sessions live in a separate user_session cookie from admin_session (T6) — the two are unrelated systems, confirmed by a test that a user session can't unlock /api/admin/*. Login and "email not registered" return the identical 401 body so the endpoint can't be used to enumerate registered emails. Every test file that spawns server.js in an isolated tmpdir now also copies db.js alongside it (dance-channel/dressup-generate/dressup-crud/ admin-pages-flow/uploads-route .test.js) — server.js requires db.js unconditionally now, so those child processes were crashing silently without it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
Backend (server.js): - T11: dance.html gets its own schedule/queue state machine, persisted to dance-state.json (gitignored, same as live-state.json). Per MVCP M.8, the schedule stores videoId references rather than inlined clip snapshots like live's does, joining back to library.json at read time. Generalized pickBestIndexByStatus/extracted pickLeastPlayedClip so both channels' schedulers share the same selection logic instead of forking it; live's random-fill pool now excludes channel:"dance" clips (and vice versa) so the two channels' filler picks stay independent, which wasn't true before this (T8's dance clips could have leaked into live.html's filler). - T12: GET /api/dance/wardrobe — public, unauthenticated list of active garments. - T15: POST /api/dance/pick {garmentId, nickname?} — free path, always uses the configured default character, reuses submitGeneration (T8/T9) whole including its M.7 dedupe cache, then hands the result to danceQueue. In-flight generations are reconciled against generationJobs on each schedule tick rather than via a promise chain, so a pick request survives a server restart the same way T8's own jobs do. - T17: separate danceViewers map and POST /api/dance/queue/vote/:id, so viewer counts and votes don't mix with live.html's. - SCHEDULE_TICK_MS env var makes both channels' tick interval configurable so tests don't wait out a real 2s. Frontend: - dance.html (T13/T14/T16): reuses live.html's visual language via live.css, adds dance.css for the 9:16 boxed player and wardrobe grid. Left column is a 衣柜/传衣服/传人 tab set (only 衣柜 is wired up — the other two need accounts/credits, dev-plan-2.md 第三/四期); right column shows the queue with vote buttons instead of live.html's chat. Age-confirmation gate (5.1节) blocks the page on first visit via a cookie, non-dismissable by clicking the backdrop. Tests: dance-channel.test.js spins up a mock H3 Console (same approach as dressup-generate.test.js) to exercise wardrobe listing, pick rejection/happy-path/dedupe, vote, and independent viewer counts end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>