From e410f7974ee60cd6806dc2ba7ee2aa7f26dd175a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Fri, 8 May 2026 21:46:11 +0800 Subject: [PATCH] 1 --- .hermes/shared-memory/pitfalls.md | 8 + ..._DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md | 15 +- ...ADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md | 24 +- server-rs/Cargo.lock | 1 - server-rs/crates/api-server/Cargo.toml | 1 - .../api-server/src/volcengine_speech.rs | 21 +- server-rs/crates/platform-speech/src/lib.rs | 2 + .../PlatformEntryFlowShellImpl.tsx | 76 +++- .../PuzzleRuntimeShell.test.tsx | 32 +- .../puzzle-runtime/PuzzleRuntimeShell.tsx | 299 ++++++------ .../RpgEntryHomeView.recharge.test.tsx | 115 +++-- src/components/rpg-entry/RpgEntryHomeView.tsx | 165 +++---- src/index.css | 424 +++++++++++++----- 13 files changed, 757 insertions(+), 426 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 6c86f356..90fc46a9 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -120,6 +120,14 @@ - 验证:`npm run api-server` 后 `/healthz` 返回 200,相关路由冒烟通过。 - 关联:`server-rs/crates/api-server/src/main.rs`、`server-rs/crates/api-server/src/app.rs`。 +## Windows api-server.exe 锁文件与强杀退出码容易混淆 + +- 现象:`cargo run -p api-server` 或 `npm run api-server` 报 `failed to remove file ... target\debug\api-server.exe`;清理旧进程后,旧终端可能继续打印 `process didn't exit successfully: server-rs\target\debug\api-server.exe (exit code: 0xffffffff)`。 +- 原因:Windows 不能覆盖仍在运行的 exe;通常是上一条 `npm run api-server` 链路仍在运行,进程树为 `npm run api-server -> node scripts/api-server-dev.mjs -> cargo run -> api-server.exe`。`0xffffffff` 常见于排障时用 `Stop-Process -Force` 强制结束旧 `api-server.exe` 后由 Cargo 回显,不一定代表新启动失败。 +- 处理:先按目标路径确认并停止本仓库的旧 `api-server.exe` 及其父级 `cargo/node/cmd` 启动链路,再重新启动;不要同时开多个 `npm run api-server`。 +- 验证:确认没有匹配 `C:\Genarrative\server-rs\target\debug\api-server.exe` 的进程后,`Remove-Item` 能删除旧 exe;随后 `npm run api-server` 启动并访问 `/healthz` 返回 200。 +- 关联:`scripts/api-server-dev.mjs`、`server-rs/crates/api-server/src/main.rs`。 + ## Windows debug 长 SSE Future 触发 api-server 断连 - 现象:前端 Vite 代理请求 `/api/runtime/creative-agent/sessions/{sessionId}/messages/stream` 报 `read ECONNRESET`,随后 `api-server.exe` 以 `0xffffffff` 退出,`dev:rust` 回收 SpacetimeDB、Vite 和后台 Vite。 diff --git a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md index 87807c71..bc63c74e 100644 --- a/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md +++ b/docs/design/PLATFORM_MOBILE_RECOMMEND_DISCOVER_DRAFT_TAB_REDESIGN_2026-05-05.md @@ -31,10 +31,13 @@ - 数据来源沿用 `featuredEntries + latestEntries` 去重后的公开作品列表。 - 首个可运行作品自动进入推荐页内嵌运行态,主视口不再展示作品封面卡。 -- 主视口占据顶部栏与底部作品区之间的主要空间,保持黑色运行容器、圆角边界和短加载态,直接承载作品启动后的玩法画面。 +- 主视口占据顶部栏与作品信息区之间的主要空间,使用平台主题 token 控制运行容器背景、边框、阴影、加载态文字和错误态按钮;亮色主题不得残留纯黑底白字加载块。 - 主视口下方展示当前作品的游玩、点赞、评论/改造等紧凑指标、作者头像、作者名与作品名,不写规则说明类文案。 -- 底部作品区使用横向滑动切换器,条目只展示作品名、类型和核心指标;点击或滑动到其他作品时切换上方运行内容。 -- 点击作品元信息仍可进入既有详情页,点赞、改造、复制作品号等完整操作继续收敛在详情页。 +- 作品信息区不再提供详情箭头或点击详情入口;点击该区域无效,上滑切换下一个推荐作品,下滑切换上一个推荐作品。 +- 用户停留在推荐页时,底部当前 Tab 从“推荐”切换为“下一个”,图标使用向下的倒三角 / 双下箭头语义,点击后切换下一个推荐作品。 +- 推荐页不再展示额外的底部作品切换块;当前作品的完整操作继续收敛在详情页和作品自身运行态中。 +- 推荐页嵌入运行只调整平台外壳容器、主题注入和玩法壳层配色,不改写作品数据、关卡设定、道具设定或图片资产。 +- 点赞、改造、复制作品号等完整操作继续收敛在详情页,详情入口由作品自身运行态或其它广场列表承接,推荐页作品信息区只负责展示和上下滑切换。 - 无数据、加载中、启动失败和暂不支持内嵌运行的作品沿用短状态文案。 桌面端仍保持现有首页布局,只把一级导航文案从“首页”改为“推荐”。 @@ -74,14 +77,16 @@ ## 7. 验收 -1. 移动端底部导航显示“推荐 / 发现 / 创作 / 草稿 / 我的”,未登录时显示“推荐 / 创作 / 发现”。 +1. 移动端底部导航非推荐页显示“推荐 / 发现 / 创作 / 草稿 / 我的”,未登录时显示“推荐 / 创作 / 发现”;停留在推荐页时当前 Tab 显示“下一个”。 2. 点击“推荐”直接看到公开作品启动后的内容,不再先看到搜索框、频道 Tab 或封面卡流。 3. 点击“发现”可看到搜索、推荐、今日、分类、排行子 Tab。 4. 点击“草稿”看到原创作页作品列表。 5. 点击“创作”只看到新建创作入口。 6. “我的”里的“玩过”弹层包含原存档列表入口,点击存档能继续恢复。 7. 移动端底部导航为悬浮胶囊样式,保留当前明暗主题色变量,不新增第三套主题。 -8. 推荐页底部作品区可横向滑动并切换作品,切换后上方运行视口同步进入对应作品内容。 +8. 推荐页不出现额外底部作品卡或横滑切换块,运行视口、加载态和错误态跟随当前明暗主题。 +9. 拼图玩法背景、HUD、按钮、弹窗、排行榜和相似作品卡跟随平台主题色;暗色主题仍保留深色游戏感,亮色主题不得出现大面积固定黑底。 +10. 推荐页作品信息区点击无效,上滑切下一个、下滑切上一个;点击底部“下一个”也切下一个作品。 ## 8. 2026-05-07 未登录三栏补充 diff --git a/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md b/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md index a062a099..53b34e35 100644 --- a/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md +++ b/docs/design/PUZZLE_RUNTIME_HEADER_AND_CLEAR_EFFECT_DESIGN_2026-04-27.md @@ -17,9 +17,8 @@ 1. 左侧保留返回按钮。 2. 中间居中展示关卡主信息: - - 第一行:拼图关卡名 - - 第二行:作者昵称 - - 第三行:`第 N 关` + - 第一行:`第 N 关` 与拼图关卡名,二者保持同一行。 + - 第二行:紧凑倒计时。 3. 右侧新增设置按钮。 同时移除以下冗余标识: @@ -31,12 +30,27 @@ ### 1.1 2026-04-30 顶栏与底部工具补充 -1. 顶栏作者信息不再只显示一行作者名,必须展示为作者头像与昵称组合;当前运行态只提供昵称时,用昵称首字生成圆形占位头像。 -2. 倒计时组件提升为顶栏中的强信息,字号、内边距和图标尺寸都需要明显大于作者昵称与关卡序号。 +1. 历史口径曾要求顶栏展示作者头像与昵称;该要求已被 2026-05-08 精简口径替代,当前拼图棋盘 HUD 不再展示作者信息。 +2. 倒计时组件保持为顶栏中的强信息,但需要采用紧凑尺寸,不得遮挡棋盘内容。 3. 底部只保留 `提示 / 原图 / 冻结` 三个功能按钮,并整体居中展示;三个按钮触控面积和图标字号都需要放大。 4. 底部不再展示“等待下一关候选”这类状态占位。通关后在三个道具按钮上方固定展示“下一关”按钮,展示条件只依赖当前关卡已通关,不依赖 `recommendedNextProfileId` 是否已有值。 5. 点击底部“下一关”按钮继续调用运行时壳层已有 `onAdvanceNextLevel` 事件;正式 run 由后端 `next-level` 选择候选,本地 run 由 `local-next-level` 生成或接续下一关,前端不在按钮层自行决定下一关来源。 +### 1.2 2026-05-08 推荐页嵌入态 HUD 精简 + +1. 拼图运行时顶栏不再展示作者头像、作者昵称或作者首字占位,作者信息只在推荐页作品信息、详情页和排行榜等非棋盘 HUD 区域展示。 +2. 顶部主信息压缩为两行:第一行 `第 N 关 + 关卡名`,第二行小号倒计时;倒计时不能使用此前的大号胶囊尺寸。 +3. 返回按钮和设置按钮上移并缩小移动端基础尺寸,减少对棋盘顶部空间的占用。 +4. 棋盘容器必须保留固定顶部安全区,确保关卡名和倒计时不会遮挡拼图内容。 +5. 推荐页嵌入时只调整外壳间距和 HUD 布局,不改写作品关卡、作者、道具、时间限制或图片资产等作品设定。 + +### 1.3 2026-05-08 主题色联动 + +1. 拼图运行态根容器、背景、棋盘底色、顶部 HUD、底部工具栏、加载态、失败弹窗、通关弹窗、道具确认弹窗、设置弹窗和相似作品卡必须通过 `src/index.css` 中的 `--puzzle-runtime-*` 主题变量控制。 +2. `platform-theme--light` 下拼图玩法背景应使用浅色平台底色与粉橙主色点缀,文字使用平台正文 token;不得继续使用固定 `bg-slate-950 text-white` 作为大面积底色。 +3. `platform-theme--dark` 下可保留深色棋盘氛围,但按钮、选中态、倒计时、弹窗边框和推荐页加载态仍要从主题 token 取色,避免局部色板漂移。 +4. 推荐页内嵌拼图时,父级必须保持 `platform-theme` 类可传递到 `PuzzleRuntimeShell`,不能让 runtime 脱离平台主题变量。 + ### 2. 拼图块显示规则 运行时单块右下角编号全部移除。 diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index c809ecda..ec124622 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -119,7 +119,6 @@ dependencies = [ "time", "tokio", "tokio-stream", - "tokio-tungstenite 0.27.0", "tower", "tower-http", "tracing", diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index 117b95b0..6dc372f8 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -42,7 +42,6 @@ shared-logging = { workspace = true } spacetime-client = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time"] } tokio-stream = { workspace = true } -tokio-tungstenite = { workspace = true } futures-util = { workspace = true } time = { workspace = true, features = ["formatting"] } tower-http = { workspace = true, features = ["trace"] } diff --git a/server-rs/crates/api-server/src/volcengine_speech.rs b/server-rs/crates/api-server/src/volcengine_speech.rs index cb631dcb..e6a5f746 100644 --- a/server-rs/crates/api-server/src/volcengine_speech.rs +++ b/server-rs/crates/api-server/src/volcengine_speech.rs @@ -6,18 +6,17 @@ use axum::{ ws::{Message as ClientWsMessage, WebSocket, WebSocketUpgrade}, }, http::{HeaderValue, StatusCode, header}, - response::{IntoResponse, Response}, + response::Response, }; use futures_util::{SinkExt, StreamExt, TryStreamExt}; use platform_speech::{ AsrAudioConfig, AsrFrameKind, PublicSpeechConfig, PublicSpeechEndpoints, SpeechError, - TtsAudioParams, TtsBidirectionClientEvent, TtsSseRequest, VolcengineSpeechClient, - VolcengineSpeechConfig, build_asr_frame, build_asr_full_client_request, + TtsAudioParams, TtsBidirectionClientEvent, TtsSseRequest, UpstreamWsError, UpstreamWsMessage, + VolcengineSpeechClient, VolcengineSpeechConfig, build_asr_frame, build_asr_full_client_request, build_tts_bidirection_frame_from_client_event, default_asr_request_payload, parse_asr_response_frame, parse_tts_response_frame, tts_response_to_client_value, }; use serde_json::{Value, json}; -use tokio_tungstenite::tungstenite::Message as UpstreamWsMessage; use tracing::{info, warn}; use crate::{ @@ -249,12 +248,12 @@ async fn proxy_asr_websocket(socket: WebSocket, client: VolcengineSpeechClient, } Ok(UpstreamWsMessage::Text(text)) => { browser_sender - .send(ClientWsMessage::Text(text)) + .send(ClientWsMessage::Text(text.to_string().into())) .await .map_err(map_client_ws_send_error)?; } - Ok(UpstreamWsMessage::Close(close)) => { - let _ = browser_sender.send(ClientWsMessage::Close(close)).await; + Ok(UpstreamWsMessage::Close(_)) => { + let _ = browser_sender.send(ClientWsMessage::Close(None)).await; break; } Ok(UpstreamWsMessage::Ping(bytes)) => { @@ -363,12 +362,12 @@ async fn proxy_tts_bidirection_websocket(socket: WebSocket, client: VolcengineSp } Ok(UpstreamWsMessage::Text(text)) => { browser_sender - .send(ClientWsMessage::Text(text)) + .send(ClientWsMessage::Text(text.to_string().into())) .await .map_err(map_client_ws_send_error)?; } - Ok(UpstreamWsMessage::Close(close)) => { - let _ = browser_sender.send(ClientWsMessage::Close(close)).await; + Ok(UpstreamWsMessage::Close(_)) => { + let _ = browser_sender.send(ClientWsMessage::Close(None)).await; break; } Ok(UpstreamWsMessage::Ping(bytes)) => { @@ -451,7 +450,7 @@ fn map_speech_error(error: SpeechError) -> AppError { } } -fn map_ws_send_error(error: tokio_tungstenite::tungstenite::Error) -> SpeechError { +fn map_ws_send_error(error: UpstreamWsError) -> SpeechError { SpeechError::Upstream(format!("发送火山语音 WebSocket 帧失败:{error}")) } diff --git a/server-rs/crates/platform-speech/src/lib.rs b/server-rs/crates/platform-speech/src/lib.rs index 2904554e..e907f194 100644 --- a/server-rs/crates/platform-speech/src/lib.rs +++ b/server-rs/crates/platform-speech/src/lib.rs @@ -17,6 +17,8 @@ use tokio_tungstenite::{ }; use uuid::Uuid; +pub use tokio_tungstenite::tungstenite::{Error as UpstreamWsError, Message as UpstreamWsMessage}; + pub const DEFAULT_ASR_WS_URL: &str = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"; pub const DEFAULT_TTS_BIDIRECTION_WS_URL: &str = "wss://openspeech.bytedance.com/api/v3/tts/bidirection"; diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 0a980202..dac4e955 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -4699,6 +4699,31 @@ export function PlatformEntryFlowShellImpl({ ], ); + const saveAndExitRecommendPuzzleRuntime = useCallback(async () => { + if (activeRecommendRuntimeKind !== 'puzzle') { + return; + } + + const currentRun = puzzleRunRef.current; + if (!currentRun) { + setActiveRecommendRuntimeKind(null); + return; + } + + // 中文注释:推荐页嵌入拼图的“保存并退出”沿用现有运行态语义; + // 正式 run 的每次交换/拖动/通关已写回后端,退出时只收口暂停和本地快照。 + const closedRun = currentRun.currentLevel + ? setLocalPuzzlePaused(currentRun, false) + : currentRun; + puzzleRunRef.current = null; + setPuzzleRun(null); + setActiveRecommendRuntimeKind(null); + + if (closedRun.currentLevel) { + setPuzzleError(null); + } + }, [activeRecommendRuntimeKind, setPuzzleError]); + const leaveAgentWorkspace = useCallback(() => { enterCreateTab(); sessionController.resetSessionViewState(); @@ -5952,6 +5977,9 @@ export function PlatformEntryFlowShellImpl({ async (entry: PlatformPublicGalleryCard) => { const entryKey = getPlatformPublicGalleryEntryKey(entry); const runtimeKind = getPlatformRecommendRuntimeKind(entry); + if (entryKey !== activeRecommendEntryKey) { + await saveAndExitRecommendPuzzleRuntime(); + } setActiveRecommendEntryKey(entryKey); setActiveRecommendRuntimeKind(runtimeKind); setActiveRecommendRuntimeError(null); @@ -6025,6 +6053,8 @@ export function PlatformEntryFlowShellImpl({ } }, [ + activeRecommendEntryKey, + saveAndExitRecommendPuzzleRuntime, selectedPuzzleDetail, setBigFishError, setMatch3DError, @@ -6037,6 +6067,38 @@ export function PlatformEntryFlowShellImpl({ startVisualNovelRunFromProfile, ], ); + const selectAdjacentRecommendRuntimeEntry = useCallback( + (direction: 1 | -1) => { + if (recommendRuntimeEntries.length === 0) { + return; + } + + const activeIndex = recommendRuntimeEntries.findIndex( + (entry) => + getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey, + ); + const baseIndex = activeIndex >= 0 ? activeIndex : 0; + const nextIndex = + (baseIndex + direction + recommendRuntimeEntries.length) % + recommendRuntimeEntries.length; + const nextEntry = recommendRuntimeEntries[nextIndex]; + if (!nextEntry) { + return; + } + if ( + getPlatformPublicGalleryEntryKey(nextEntry) === activeRecommendEntryKey + ) { + return; + } + + void selectRecommendRuntimeEntry(nextEntry); + }, + [ + activeRecommendEntryKey, + recommendRuntimeEntries, + selectRecommendRuntimeEntry, + ], + ); const recommendRuntimeContent = useMemo(() => { if ( @@ -6153,6 +6215,8 @@ export function PlatformEntryFlowShellImpl({ } error={puzzleError} embedded + hideBackButton + hideExitControls onBack={() => { setActiveRecommendRuntimeKind(null); }} @@ -6270,7 +6334,7 @@ export function PlatformEntryFlowShellImpl({ } return ( -