fix: remove recommend login gate
This commit is contained in:
@@ -98,6 +98,8 @@ npm run check:server-rs-ddd
|
||||
|
||||
该拆分只改变 `api-server` 文件组织,不改变 `/api/runtime/puzzle/*` route、DTO、error envelope、SpacetimeDB schema、公开 gallery cache 语义或计费语义;后续继续细分时也必须先保持行为不变,再单独讨论领域规则下沉。
|
||||
|
||||
`/api/runtime/puzzle/runs*` 当前接受 `RuntimePrincipal`,可同时识别登录用户 Bearer 和 runtime guest token。推荐页嵌入运行态的正式开局、交换、拖拽、下一关、暂停、道具与排行榜请求,应由前端在登录态下继续携带账号 access token;匿名游客仅在确认为未登录时走 runtime guest token。不要再把拼图 runtime 当成只认普通 Bearer 的纯账号接口。
|
||||
|
||||
抓大鹅 Match3D `api-server` 内部拆分:
|
||||
|
||||
- `server-rs/crates/api-server/src/modules/match3d.rs` 继续负责路由装配和 body limit;对外 handler 名称保持不变。
|
||||
|
||||
@@ -152,7 +152,7 @@ Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`:
|
||||
|
||||
```bash
|
||||
cargo check -p api-server --manifest-path server-rs/Cargo.toml
|
||||
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend page can enter runtime without login gate|logged out desktop recommend page renders runtime directly without login gate"
|
||||
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend tab enters runtime without login modal|logged out desktop recommend page renders runtime directly|logged out desktop recommend rail enters runtime without login modal"
|
||||
```
|
||||
|
||||
涉及 SpacetimeDB schema 时必须补:
|
||||
|
||||
@@ -92,6 +92,7 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `次级入口 >
|
||||
- 结果页单关测试只能把完整草稿持久化,并通过 `levelId` 指定运行态起始关卡;不得把单关快照作为整份草稿调用 `updatePuzzleWork`,否则 source session 和作品 profile 的 `levels` 会被覆盖成单关,退出重进后其它关卡会丢失。
|
||||
- 结果页生成关卡图时若关卡名为空,前端必须传 `shouldAutoNameLevel=true`,后端复用首关命名契约先按画面描述生成关卡名,再在图片生成后用视觉命名结果精修,并把生成名和 UI 背景提示词随本次关卡快照写回。
|
||||
- 拼图运行态背景优先读取当前关卡 `levelBackgroundImageSrc/levelBackgroundImageObjectKey`,旧数据才兼容 `uiBackgroundImageSrc/uiBackgroundImageObjectKey`;本地试玩、直达指定关卡和正式 `next-level` 推进时,目标关卡缺关卡背景时必须继承同作品首个可用关卡背景,仍缺失时才沿用当前运行态快照背景或默认 UI。运行态按钮视觉优先读取当前关卡 `uiSpritesheetImageSrc/uiSpritesheetImageObjectKey`,先按透明 alpha 自动边界检测识别 spritesheet 中的独立按钮展示矩形,再按原图位置从左到右、从上到下映射到返回、设置、下一关、提示、原图、冻结;同一组件还要按较高 alpha 阈值派生紧致点击热区,透明留白和柔边低 alpha 区域尽量不响应点击。检测失败时回退旧固定六格裁切,缺失时才用现有图标按钮兜底。有 spritesheet 时,返回和设置按钮的点击容器只提供透明点击区,不再叠加默认白色圆形底;底部提示、原图、冻结三枚素材按检测矩形的原始宽高比显示,不能强行拉伸成正圆或铺满整列。底部道具区不再使用连片胶囊背景,提示、原图、冻结三个按钮均匀分布;运行态只展示按钮素材本身,不额外叠加“提示 / 原图 / 冻结”文字。
|
||||
- 推荐页本身不是登录门禁入口,未登录用户点击底部或侧边栏的推荐 Tab 应直接进入嵌入运行态,不主动打开登录弹窗。推荐页嵌入运行态必须按真实身份分流:已登录用户或本地已有 access token 时,启动拼图和后续排行榜 / 下一关等正式请求继续走账号 Bearer;只有确认为匿名访客时才申请并透传 runtime guest token。`/api/runtime/puzzle/runs*` 后端统一接受 `RuntimePrincipal`,可识别账号用户和匿名 runtime guest;推荐卡片的后台读写请求仍使用 local auth impact,避免单卡 401 清空整站登录态。创作、个人作品、删除、发布、Remix 等账号或所有权动作仍保持普通用户鉴权。
|
||||
- 拼图运行态棋盘不叠加分块蒙版、描边、阴影、选中底色或合并块 SVG 轮廓;拼图片本体需要裁切为圆角形状,单块使用独立圆角裁切,合并块使用 SVG 原生 `clipPath` 裁切整体外轮廓,外凸角和内凹角分别计算半径,内凹角半径要比外凸角更明显以避免手机 WebView 中看起来仍是直角。原图道具只在用户主动确认后打开独立原图查看层,不在当前拼图棋盘上叠加原图。
|
||||
- 拼图运行态拖拽必须完全跟随手指或鼠标位置,`pointermove` 期间即时写入可见拼块的 transform,不依赖等待后端回包、React 重渲染或下一帧动画队列;进入拖动后不展示拼块选中态或“已选择”提示,松手后再提交目标格同步规则真相。
|
||||
- 拼图运行态的提示、设置等点击弹层跟随当前运行态主色主题,使用普通圆角主题面板,不复用像素九宫格素材框。
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||
puzzle::{
|
||||
advance_puzzle_next_level, claim_puzzle_work_point_incentive, create_puzzle_agent_session,
|
||||
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
|
||||
@@ -130,56 +130,56 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/runtime/puzzle/runs",
|
||||
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}",
|
||||
get(get_puzzle_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/swap",
|
||||
post(swap_puzzle_pieces).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/drag",
|
||||
post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/next-level",
|
||||
post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/pause",
|
||||
post(update_puzzle_run_pause).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/props",
|
||||
post(use_puzzle_runtime_prop).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs/{run_id}/leaderboard",
|
||||
post(submit_puzzle_leaderboard).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
require_runtime_principal_auth,
|
||||
)),
|
||||
)
|
||||
.with_state(PuzzleApiState::from_ref(&state))
|
||||
|
||||
@@ -76,7 +76,7 @@ use crate::{
|
||||
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
|
||||
should_skip_asset_operation_billing_for_connectivity,
|
||||
},
|
||||
auth::AuthenticatedAccessToken,
|
||||
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
|
||||
http_error::AppError,
|
||||
llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL},
|
||||
|
||||
@@ -1666,7 +1666,7 @@ pub async fn remix_puzzle_gallery_work(
|
||||
pub async fn start_puzzle_run(
|
||||
State(state): State<PuzzleApiState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<StartPuzzleRunRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -1690,7 +1690,7 @@ pub async fn start_puzzle_run(
|
||||
.spacetime_client()
|
||||
.start_puzzle_run(PuzzleRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id("puzzle-run-"),
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
profile_id: payload.profile_id.clone(),
|
||||
level_id: payload.level_id.clone(),
|
||||
started_at_micros: current_utc_micros(),
|
||||
@@ -1707,16 +1707,18 @@ pub async fn start_puzzle_run(
|
||||
record_puzzle_work_play_start_after_success(
|
||||
&state,
|
||||
&request_context,
|
||||
WorkPlayTrackingDraft::new(
|
||||
WorkPlayTrackingDraft::runtime_principal(
|
||||
"puzzle",
|
||||
payload.profile_id.clone(),
|
||||
&authenticated,
|
||||
&principal,
|
||||
"/api/runtime/puzzle/...",
|
||||
)
|
||||
.profile_id(payload.profile_id.clone())
|
||||
.owner_user_id(principal.subject().to_string())
|
||||
.extra(json!({
|
||||
"levelId": payload.level_id,
|
||||
"runId": run.run_id,
|
||||
"principalKind": principal.kind().as_str(),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
@@ -1733,13 +1735,13 @@ pub async fn get_puzzle_run(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_puzzle_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.get_puzzle_run(run_id, principal.subject().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
@@ -1761,7 +1763,7 @@ pub async fn swap_puzzle_pieces(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<SwapPuzzlePiecesRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -1792,7 +1794,7 @@ pub async fn swap_puzzle_pieces(
|
||||
.spacetime_client()
|
||||
.swap_puzzle_pieces(PuzzleRunSwapRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
first_piece_id: payload.first_piece_id,
|
||||
second_piece_id: payload.second_piece_id,
|
||||
swapped_at_micros: current_utc_micros(),
|
||||
@@ -1818,7 +1820,7 @@ pub async fn drag_puzzle_piece_or_group(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<DragPuzzlePieceRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -1843,7 +1845,7 @@ pub async fn drag_puzzle_piece_or_group(
|
||||
.spacetime_client()
|
||||
.drag_puzzle_piece_or_group(PuzzleRunDragRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
piece_id: payload.piece_id,
|
||||
target_row: payload.target_row,
|
||||
target_col: payload.target_col,
|
||||
@@ -1870,7 +1872,7 @@ pub async fn advance_puzzle_next_level(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<AdvancePuzzleNextLevelRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
|
||||
@@ -1897,7 +1899,7 @@ pub async fn advance_puzzle_next_level(
|
||||
.spacetime_client()
|
||||
.advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
target_profile_id: payload.target_profile_id,
|
||||
advanced_at_micros: current_utc_micros(),
|
||||
})
|
||||
@@ -1922,7 +1924,7 @@ pub async fn update_puzzle_run_pause(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<UpdatePuzzleRuntimePauseRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -1941,7 +1943,7 @@ pub async fn update_puzzle_run_pause(
|
||||
.spacetime_client()
|
||||
.update_puzzle_run_pause(PuzzleRunPauseRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
paused: payload.paused,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
@@ -1966,7 +1968,7 @@ pub async fn use_puzzle_runtime_prop(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<UsePuzzleRuntimePropRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -1987,7 +1989,7 @@ pub async fn use_puzzle_runtime_prop(
|
||||
"propKind",
|
||||
)?;
|
||||
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let owner_user_id = principal.subject().to_string();
|
||||
let prop_kind = payload.prop_kind.trim().to_string();
|
||||
let billing_asset_kind = match prop_kind.as_str() {
|
||||
"hint" => "puzzle_prop_hint",
|
||||
@@ -2064,7 +2066,7 @@ pub async fn submit_puzzle_leaderboard(
|
||||
State(state): State<PuzzleApiState>,
|
||||
AxumPath(run_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Extension(principal): Extension<RuntimePrincipal>,
|
||||
payload: Result<Json<SubmitPuzzleLeaderboardRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
@@ -2084,7 +2086,7 @@ pub async fn submit_puzzle_leaderboard(
|
||||
.spacetime_client()
|
||||
.submit_puzzle_leaderboard_entry(PuzzleLeaderboardSubmitRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
profile_id: payload.profile_id,
|
||||
grid_size: payload.grid_size,
|
||||
elapsed_ms: payload.elapsed_ms.max(1_000),
|
||||
|
||||
@@ -115,6 +115,7 @@ import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFound
|
||||
import {
|
||||
ApiClientError,
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||
getStoredAccessToken,
|
||||
} from '../../services/apiClient';
|
||||
import {
|
||||
ensureRuntimeGuestToken,
|
||||
@@ -559,6 +560,25 @@ async function buildRecommendRuntimeGuestOptions() {
|
||||
runtimeGuestToken: token,
|
||||
};
|
||||
}
|
||||
function shouldUseRecommendRuntimeGuestAuth(
|
||||
authUi: { user?: { id?: string } | null } | null | undefined,
|
||||
) {
|
||||
return !authUi?.user?.id?.trim() && !getStoredAccessToken();
|
||||
}
|
||||
async function buildRecommendRuntimeAuthOptions(
|
||||
authUi: { user?: { id?: string } | null } | null | undefined,
|
||||
embedded?: boolean,
|
||||
) {
|
||||
if (!embedded) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
|
||||
return buildRecommendRuntimeGuestOptions();
|
||||
}
|
||||
|
||||
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
}
|
||||
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
||||
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
||||
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
||||
@@ -7386,9 +7406,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: targetProfileId,
|
||||
mode: 'play' as const,
|
||||
};
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startVisualNovelRun(
|
||||
targetProfileId,
|
||||
@@ -7419,6 +7440,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
setSelectionStage,
|
||||
@@ -7442,7 +7464,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'visual-novel'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const nextRun = await streamVisualNovelRuntimeAction(
|
||||
visualNovelRun.runId,
|
||||
@@ -7460,6 +7482,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
isVisualNovelBusy,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
@@ -7868,9 +7891,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||
@@ -7898,7 +7922,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
},
|
||||
[setSelectionStage],
|
||||
[authUi, setSelectionStage],
|
||||
);
|
||||
|
||||
const restartJumpHopRuntimeRun = useCallback(async () => {
|
||||
@@ -8205,9 +8229,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
options.embedded
|
||||
@@ -8237,7 +8262,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[setSelectionStage],
|
||||
[authUi, setSelectionStage],
|
||||
);
|
||||
|
||||
const checkpointWoodenFishRuntimeRun = useCallback(
|
||||
@@ -8640,12 +8665,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: item.profileId,
|
||||
levelId: levelId ?? null,
|
||||
};
|
||||
const runtimeGuestOptions = options.embedded
|
||||
const canUseRuntimeGuestAuth =
|
||||
options.embedded || options.authMode === 'isolated';
|
||||
const useRuntimeGuestAuth =
|
||||
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||
const runtimeGuestOptions = useRuntimeGuestAuth
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const authMode = options.embedded
|
||||
? 'isolated'
|
||||
: (options.authMode ?? 'default');
|
||||
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
|
||||
const { run } =
|
||||
authMode === 'isolated'
|
||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||
@@ -8692,6 +8719,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isPuzzleBusy,
|
||||
authUi,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
@@ -8744,9 +8772,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
runtimeProfile.generatedBackgroundAsset,
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeOptions = {
|
||||
...runtimeGuestOptions,
|
||||
...(typeof options.itemTypeCountOverride === 'number'
|
||||
@@ -8793,6 +8822,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isMatch3DBusy,
|
||||
authUi,
|
||||
match3dFlow,
|
||||
match3dRuntimeAdapter,
|
||||
resolveMatch3DErrorMessage,
|
||||
@@ -8816,9 +8846,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleError(null);
|
||||
|
||||
try {
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||
: await startSquareHoleRun(profile.profileId);
|
||||
@@ -8852,6 +8883,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isSquareHoleBusy,
|
||||
authUi,
|
||||
resolveSquareHoleErrorMessage,
|
||||
setSelectionStage,
|
||||
setSquareHoleError,
|
||||
@@ -8974,7 +9006,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const { run } = await submitBigFishRuntimeInput(
|
||||
bigFishRun.runId,
|
||||
@@ -8992,6 +9024,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
bigFishRun,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
@@ -9008,10 +9041,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
const reportPromise =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? recordBigFishPlay(
|
||||
sessionId,
|
||||
{ elapsedMs },
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true).then(
|
||||
(runtimeAuthOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
|
||||
)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs });
|
||||
void reportPromise.catch((error) => {
|
||||
@@ -9021,6 +9053,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
}, [
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
bigFishRun?.sessionId,
|
||||
bigFishRuntimeStartedAt,
|
||||
resolveBigFishErrorMessage,
|
||||
@@ -11315,9 +11348,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeReturnStage(returnStage);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||
: await startBigFishRuntimeRun(sessionId);
|
||||
@@ -11345,7 +11379,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
||||
[authUi, bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
||||
);
|
||||
|
||||
const startBarkBattleRunFromWork = useCallback(
|
||||
@@ -11365,9 +11399,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
||||
setBarkBattleRuntimeReturnStage(returnStage);
|
||||
try {
|
||||
const runtimeGuestOptions = options.embedded
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runResponse = options.embedded
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||
: await startBarkBattleRun(item.workId);
|
||||
@@ -11390,7 +11425,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[resolveBarkBattleErrorMessage, setSelectionStage],
|
||||
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
|
||||
);
|
||||
|
||||
const startSelectedPublicWork = useCallback(() => {
|
||||
|
||||
@@ -6121,11 +6121,52 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
|
||||
const publishedPuzzleWork = {
|
||||
workId: 'puzzle-work-public-2',
|
||||
profileId: 'puzzle-profile-public-2',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'puzzle-session-public-2',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '星桥机关',
|
||||
summary: '旋转碎片并接通星桥机关。',
|
||||
themeTags: ['机关', '星桥'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
playCount: 3,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
} satisfies PuzzleWorkSummary;
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [publishedPuzzleWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
|
||||
item: publishedPuzzleWork,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: 'puzzle-profile-public-2',
|
||||
levelId: null,
|
||||
},
|
||||
);
|
||||
});
|
||||
expect(vi.mocked(startPuzzleRun).mock.calls[0]?.[1]).not.toEqual(
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-1',
|
||||
@@ -7135,7 +7176,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
vi.mocked(listProfileSaveArchives).mockClear();
|
||||
vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
|
||||
@@ -7159,7 +7199,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
elapsedMs: 18_000,
|
||||
nickname: '测试玩家',
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7180,7 +7219,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedFirstLevel.runId,
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -7343,7 +7381,6 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedThirdLevel.runId,
|
||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
@@ -7527,7 +7564,6 @@ test('recommend puzzle remix return restarts recommendation instead of stale loa
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
|
||||
|
||||
@@ -2697,7 +2697,7 @@ test('logged out mobile shell defaults to discover tab', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('logged out recommend tab opens login modal and shows cover only', async () => {
|
||||
test('logged out recommend tab enters runtime without login modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
@@ -2712,20 +2712,18 @@ test('logged out recommend tab opens login modal and shows cover only', async ()
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
container.querySelector('.platform-recommend-cover-only'),
|
||||
).toBeTruthy();
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
|
||||
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('.platform-mobile-entry-shell--recommend'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
||||
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(screen.getByLabelText('奇幻拼图 作品信息')).toBeTruthy();
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('logged out recommend cover opens login modal again', async () => {
|
||||
test('logged out recommend page keeps runtime visible without login gate', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
@@ -2741,12 +2739,9 @@ test('logged out recommend cover opens login modal again', async () => {
|
||||
await user.click(
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /登录后游玩 奇幻拼图/u }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).toHaveBeenCalledTimes(2);
|
||||
expect(openLoginModal).toHaveBeenLastCalledWith();
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2780,6 +2775,26 @@ test('logged out recommend page can enter runtime without login gate', () => {
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logged out desktop recommend rail enters runtime without login modal', async () => {
|
||||
mockDesktopLayout();
|
||||
const user = userEvent.setup();
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
renderLoggedOutHomeView(
|
||||
openLoginModal,
|
||||
{
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
},
|
||||
'category',
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '推荐' }));
|
||||
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged in recommend page uses gated recommend detail callback', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
|
||||
@@ -5372,7 +5372,7 @@ export function RpgEntryHomeView({
|
||||
{recommendRuntimeError}
|
||||
</button>
|
||||
</section>
|
||||
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
|
||||
) : isStartingRecommendEntry ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<div className="platform-recommend-runtime-state">加载中...</div>
|
||||
</section>
|
||||
@@ -6761,12 +6761,6 @@ export function RpgEntryHomeView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
@@ -6924,12 +6918,6 @@ export function RpgEntryHomeView({
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
onClick={() => {
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -351,7 +351,7 @@ export function useRpgEntryBootstrap(
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口。
|
||||
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
|
||||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user