• 参考 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
    yibo authored
     
    Browse File »

  • /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
    yibo authored
     
    Browse File »

  • 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
    yibo authored
     
    Browse File »

  • 需求文档 7.4 节's stated floor: an expired user_paid character (T36)
    or its videos must stay individually retrievable by their owner —
    "下线=不再出现在公开直播间和其他人的衣柜/随机池里",不是"下线=消失".
    
    GET /api/dance/my-works (login required, no emailVerified gate — this
    reads your own past purchases, doesn't spend anything) returns the
    requesting user's own characters/garments (ownerUserId match) each
    joined with their generated videos from library.json. New standalone
    page my-works.html (matches live.css's visual language) renders two
    sections with inline video players (same preload="none" pattern as
    admin.html), a status badge (播放中/已下线 for characters, 衣柜中/已下架
    for garments), and a login gate for logged-out visitors — reusing
    GET /api/dance/my-works's 401 rather than duplicating dance.html's auth
    modal in a second page. Linked from dance.html's header once logged in.
    
    my-works.test.js: 401 without a session; a real user who uploaded one
    garment (T27) and one 1-segment character (T35, deliberately expired
    by test time) sees both with their videos still attached; a second,
    unrelated user sees neither.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • Same shape as T28's 传衣服 panel: file input, live price pulled from
    GET /api/pricing (今 spend_character_per_segment × segmentsPerUnit,
    not hardcoded), a garment picker (随机搭配 default, or any wardrobe
    item, 需求文档 3.3 第 4 点), a segments-to-buy input (1-20, matches the
    server-side clamp), not-logged-in opens the existing login modal
    instead of failing silently, insufficient balance points at the topup
    flow. Success message is visually distinct ("boosted", yellow box,
    🎉 已插队播放") from mode 1/2's plain "生成中" — T37's explicit
    acceptance criterion that a paid_boost submission must be recognizable
    as skipping the vote queue, not just another queue entry.
    
    Also fixes a real bug this surfaced via a live smoke test (not just
    new to T37 — T28's submitGarmentUpload had the identical bug): both
    upload handlers set the success/status message and *then* called
    `await refreshAuthState()`, which re-renders the whole panel
    (creditBalance changed -> renderAuthButton -> renderWardrobe) and wipes
    whatever was just set. Both now set the message after refreshAuthState()
    resolves, re-querying the fresh DOM it produced.
    
    Seeded the live pricing.json with spend_character_per_segment (it only
    existed in pricing.js's DEFAULTS, which don't retroactively apply to
    an already-existing file — the same class of gap as the isDefault
    character fix two commits ago).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • T28 replaces the "开发中敬请期待" placeholder on dance.html's 传衣服
    tab with a real upload panel: file input, the live spend_garment price
    pulled from the new public GET /api/pricing (not hardcoded, per
    dev-plan-4.md's acceptance criterion), and a submit flow that POSTs to
    T27's /api/dance/upload-garment. Not logged in opens the existing T22
    login modal instead of failing silently; not email-verified shows an
    inline message; a 402 (insufficient balance) points at the new topup
    flow; success refreshes the balance and immediately polls the queue,
    same as the free wardrobe pick's own pollState() call.
    
    T29 adds a 充值 pill (visible once logged in) opening a tier picker
    built from GET /api/pricing's topup_tier entries, POSTing to T24's
    /api/credits/checkout and navigating to whatever URL comes back (the
    mock completion page today, Stripe's hosted Checkout once a real key
    lands tomorrow — both redirect back to dance.html?topup=success/
    cancelled). A one-shot banner reads that query param on load, then
    scrubs it via history.replaceState. The account button's own label
    now also shows creditBalance, riding on the field GET /api/auth/me
    already started returning in T24.
    
    Manually verified end to end against the live server (localhost:8934):
    register -> verify -> mock checkout -> balance updates in the header
    -> topup banner -> upload panel shows the live price and opens the
    login modal when logged out. The upload itself surfaced that
    production's one seed character had never been marked isDefault (see
    the preceding commit) — not a bug in this code, but blocked a live
    walk of the full generate path until fixed.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse File »
  • Reuses the existing nickname-modal/.nickname-box comic-panel look (a
    new .auth-error line and a plain-text auth-link-btn are the only new
    CSS) rather than inventing a second visual language. A "登录" topbar
    button opens the modal (email/password, toggle between login/
    register, cancel); once logged in it becomes "<nickname or email
    prefix> · 退出".
    
    Entirely optional, same as T19-T21 designed it: the modal's only
    trigger is that one button, nothing forces it open, and free browsing/
    voting keeps working on the plain viewer_uid cookie exactly as before.
    Manually verified end to end against the real running server (Chrome):
    register auto-logs in, an existing viewer_nickname cookie bound onto
    the new account and shown in the button, logout reverts the UI.
    
    auth-ui.test.js is a structural check (id/getElementById consistency,
    all four /api/auth/* endpoints wired, modal starts hidden) since no
    DOM/browser tooling is available to `node --test` here to render the
    pages for a full interaction test.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse File »
  • 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>
    yibo authored
     
    Browse File »