feat: unify recommend anonymous runtime guest auth
- Route recommended runtime launches through shared runtime guest token handling - Extend recommend-page anonymous play beyond jump-hop - Add regression coverage for runtime guest launch clients - Update docs to reflect the full anonymous-play matrix
This commit is contained in:
@@ -124,7 +124,7 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `常用功能 >
|
|||||||
|
|
||||||
删除等破坏性动作当前未接入 jump-hop 删除 API;如果后续要在作品架提供删除入口,必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。
|
删除等破坏性动作当前未接入 jump-hop 删除 API;如果后续要在作品架提供删除入口,必须先补齐后端/SpacetimeDB/前端整条删除链路,再开放按钮。
|
||||||
|
|
||||||
推荐页允许未登录直接游玩跳一跳运行态;`/api/runtime/jump-hop/runs`、`/jump` 和 `/restart` 采用可选鉴权,未登录时仍记录 `work_play_start`,但埋点需标记匿名语义。
|
推荐页匿名游玩不再限定为跳一跳。推荐页嵌入运行态启动时统一先申请短期 Runtime Guest Token,并只把它作为局部请求头传给运行态客户端,不写入全局登录态、不触发 refresh,也不把匿名流量伪装成普通用户。当前覆盖矩阵为:跳一跳、视觉小说、抓大鹅 Match3D、方洞挑战、拼图、敲木鱼、大鱼吃小鱼、汪汪声浪。每个模板的启动请求、推荐页内后续运行态动作以及需要上报的 play/finish/leaderboard/next-level 类请求,都必须继续透传 runtime guest token;公开读取入口仍可匿名读取,创作、个人作品、删除、发布、Remix 等账号/所有权动作仍保持普通用户鉴权。
|
||||||
|
|
||||||
## 敲木鱼
|
## 敲木鱼
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ export type PublicUserSearchResponse = {
|
|||||||
user: PublicUserSummary;
|
user: PublicUserSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RuntimeGuestTokenResponse = {
|
||||||
|
token: string;
|
||||||
|
expiresAt: string;
|
||||||
|
subject: string;
|
||||||
|
scope: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthEntryRequest = {
|
export type AuthEntryRequest = {
|
||||||
phone: string;
|
phone: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ use axum::{
|
|||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use platform_auth::{
|
use platform_auth::{
|
||||||
AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token, verify_access_token,
|
AccessTokenClaims, AuthProvider, BindingStatus, RuntimeGuestTokenClaims,
|
||||||
|
RuntimeGuestTokenClaimsInput, RUNTIME_GUEST_SCOPE_PUBLIC_PLAY, read_refresh_session_token,
|
||||||
|
sign_runtime_guest_token, verify_access_token, verify_runtime_guest_token,
|
||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
use shared_contracts::auth::RuntimeGuestTokenResponse;
|
||||||
|
use shared_kernel::{format_rfc3339, new_uuid_simple_string};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -34,6 +38,18 @@ pub struct RefreshSessionToken {
|
|||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum RuntimePrincipal {
|
||||||
|
User(AuthenticatedAccessToken),
|
||||||
|
Guest(RuntimeGuestTokenClaims),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RuntimePrincipalKind {
|
||||||
|
User,
|
||||||
|
Guest,
|
||||||
|
}
|
||||||
|
|
||||||
impl AuthenticatedAccessToken {
|
impl AuthenticatedAccessToken {
|
||||||
pub fn new(claims: AccessTokenClaims) -> Self {
|
pub fn new(claims: AccessTokenClaims) -> Self {
|
||||||
Self { claims }
|
Self { claims }
|
||||||
@@ -54,6 +70,66 @@ impl RefreshSessionToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RuntimePrincipal {
|
||||||
|
pub fn subject(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::User(authenticated) => authenticated.claims().user_id(),
|
||||||
|
Self::Guest(claims) => claims.subject(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> RuntimePrincipalKind {
|
||||||
|
match self {
|
||||||
|
Self::User(_) => RuntimePrincipalKind::User,
|
||||||
|
Self::Guest(_) => RuntimePrincipalKind::Guest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimePrincipalKind {
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::User => "user",
|
||||||
|
Self::Guest => "guest",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn issue_runtime_guest_token(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let issued_at = OffsetDateTime::now_utc();
|
||||||
|
let claims = RuntimeGuestTokenClaims::from_input(
|
||||||
|
RuntimeGuestTokenClaimsInput {
|
||||||
|
subject: format!("guest-runtime-{}", new_uuid_simple_string()),
|
||||||
|
scope: RUNTIME_GUEST_SCOPE_PUBLIC_PLAY.to_string(),
|
||||||
|
},
|
||||||
|
state.auth_jwt_config(),
|
||||||
|
issued_at,
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||||
|
})?;
|
||||||
|
let token = sign_runtime_guest_token(&claims, state.auth_jwt_config()).map_err(|error| {
|
||||||
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||||
|
})?;
|
||||||
|
let expires_at = OffsetDateTime::from_unix_timestamp(claims.expires_at_unix() as i64)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| format_rfc3339(value).ok())
|
||||||
|
.unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string());
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
RuntimeGuestTokenResponse {
|
||||||
|
token,
|
||||||
|
expires_at,
|
||||||
|
subject: claims.subject().to_string(),
|
||||||
|
scope: claims.scope().to_string(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn require_bearer_auth(
|
pub async fn require_bearer_auth(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
mut request: Request,
|
mut request: Request,
|
||||||
@@ -70,30 +146,71 @@ pub async fn require_bearer_auth(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn attach_optional_bearer_auth(
|
pub async fn require_runtime_principal_auth(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
mut request: Request,
|
mut request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
if let Some(authenticated) = authenticate_request(&state, &request)? {
|
let Some(principal) = authenticate_runtime_principal(&state, &request)? else {
|
||||||
request.extensions_mut().insert(authenticated.clone());
|
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
|
||||||
|
};
|
||||||
|
request.extensions_mut().insert(principal.clone());
|
||||||
|
|
||||||
let mut response = next.run(request).await;
|
let mut response = next.run(request).await;
|
||||||
response.extensions_mut().insert(authenticated);
|
response.extensions_mut().insert(principal);
|
||||||
return Ok(response);
|
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(next.run(request).await)
|
fn authenticate_runtime_principal(
|
||||||
|
state: &AppState,
|
||||||
|
request: &Request,
|
||||||
|
) -> Result<Option<RuntimePrincipal>, AppError> {
|
||||||
|
if !request.headers().contains_key(AUTHORIZATION) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
match authenticate_request(state, request) {
|
||||||
|
Ok(Some(authenticated)) => Ok(Some(RuntimePrincipal::User(authenticated))),
|
||||||
|
Ok(None) => Ok(None),
|
||||||
|
Err(_) => {
|
||||||
|
let bearer_token = extract_bearer_token(request.headers())?;
|
||||||
|
let request_id = request
|
||||||
|
.extensions()
|
||||||
|
.get::<RequestContext>()
|
||||||
|
.map(|context| context.request_id().to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
let claims = verify_runtime_guest_token(&bearer_token, state.auth_jwt_config())
|
||||||
|
.map_err(|error| {
|
||||||
|
warn!(
|
||||||
|
%request_id,
|
||||||
|
error = %error,
|
||||||
|
"runtime guest JWT 校验失败"
|
||||||
|
);
|
||||||
|
AppError::from_status(StatusCode::UNAUTHORIZED)
|
||||||
|
})?;
|
||||||
|
if claims.scope() != RUNTIME_GUEST_SCOPE_PUBLIC_PLAY {
|
||||||
|
warn!(
|
||||||
|
%request_id,
|
||||||
|
scope = %claims.scope(),
|
||||||
|
"runtime guest JWT scope 非法"
|
||||||
|
);
|
||||||
|
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
|
||||||
|
}
|
||||||
|
Ok(Some(RuntimePrincipal::Guest(claims)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticate_request(
|
fn authenticate_request(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
) -> Result<Option<AuthenticatedAccessToken>, AppError> {
|
) -> Result<Option<AuthenticatedAccessToken>, AppError> {
|
||||||
if allows_internal_forwarded_auth(request.uri().path())
|
if allows_internal_forwarded_auth(request.uri().path()) {
|
||||||
&& let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers())
|
if let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers()) {
|
||||||
{
|
|
||||||
return Ok(Some(AuthenticatedAccessToken::new(claims)));
|
return Ok(Some(AuthenticatedAccessToken::new(claims)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !request.headers().contains_key(AUTHORIZATION) {
|
if !request.headers().contains_key(AUTHORIZATION) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use std::{collections::BTreeMap, time::{SystemTime, UNIX_EPOCH}};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
auth::AuthenticatedAccessToken,
|
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
generated_asset_sheets::{
|
generated_asset_sheets::{
|
||||||
GeneratedAssetSheetPromptInput, build_generated_asset_sheet_prompt,
|
GeneratedAssetSheetPromptInput, build_generated_asset_sheet_prompt,
|
||||||
@@ -51,7 +51,6 @@ const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation";
|
|||||||
const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime";
|
const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime";
|
||||||
const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop";
|
const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop";
|
||||||
const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳";
|
const JUMP_HOP_TEMPLATE_NAME: &str = "跳一跳";
|
||||||
const JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID: &str = "anonymous-runtime";
|
|
||||||
const JUMP_HOP_RUNTIME_RUNS_ROUTE: &str = "/api/runtime/jump-hop/runs";
|
const JUMP_HOP_RUNTIME_RUNS_ROUTE: &str = "/api/runtime/jump-hop/runs";
|
||||||
|
|
||||||
pub async fn create_jump_hop_session(
|
pub async fn create_jump_hop_session(
|
||||||
@@ -231,15 +230,13 @@ pub async fn get_jump_hop_runtime_work(
|
|||||||
pub async fn start_jump_hop_run(
|
pub async fn start_jump_hop_run(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<JumpHopStartRunRequest>, JsonRejection>,
|
payload: Result<Json<JumpHopStartRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
||||||
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
|
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
|
||||||
let authenticated = maybe_authenticated.as_ref().map(|Extension(authenticated)| authenticated);
|
let owner_user_id = principal.subject().to_string();
|
||||||
let owner_user_id = authenticated
|
let principal_kind = principal.kind().as_str();
|
||||||
.map(|authenticated| authenticated.claims().user_id().to_string())
|
|
||||||
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
|
|
||||||
let run = state
|
let run = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.start_jump_hop_run(payload, owner_user_id.clone())
|
.start_jump_hop_run(payload, owner_user_id.clone())
|
||||||
@@ -256,7 +253,7 @@ pub async fn start_jump_hop_run(
|
|||||||
&state,
|
&state,
|
||||||
&request_context,
|
&request_context,
|
||||||
build_jump_hop_work_play_tracking_draft(
|
build_jump_hop_work_play_tracking_draft(
|
||||||
authenticated,
|
&principal,
|
||||||
run.profile_id.clone(),
|
run.profile_id.clone(),
|
||||||
JUMP_HOP_RUNTIME_RUNS_ROUTE,
|
JUMP_HOP_RUNTIME_RUNS_ROUTE,
|
||||||
)
|
)
|
||||||
@@ -265,7 +262,7 @@ pub async fn start_jump_hop_run(
|
|||||||
.profile_id(run.profile_id.clone())
|
.profile_id(run.profile_id.clone())
|
||||||
.extra(json!({
|
.extra(json!({
|
||||||
"runStatus": run.status,
|
"runStatus": run.status,
|
||||||
"isAnonymous": maybe_authenticated.is_none(),
|
"principalKind": principal_kind,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -280,15 +277,12 @@ pub async fn jump_hop_run_jump(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(run_id): Path<String>,
|
Path(run_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<JumpHopJumpRequest>, JsonRejection>,
|
payload: Result<Json<JumpHopJumpRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||||
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
||||||
let owner_user_id = maybe_authenticated
|
let owner_user_id = principal.subject().to_string();
|
||||||
.as_ref()
|
|
||||||
.map(|Extension(authenticated)| authenticated.claims().user_id().to_string())
|
|
||||||
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
|
|
||||||
let run = state
|
let run = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.jump_hop_run_jump(run_id, owner_user_id, payload)
|
.jump_hop_run_jump(run_id, owner_user_id, payload)
|
||||||
@@ -311,15 +305,12 @@ pub async fn restart_jump_hop_run(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(run_id): Path<String>,
|
Path(run_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<JumpHopRestartRunRequest>, JsonRejection>,
|
payload: Result<Json<JumpHopRestartRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||||
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
|
||||||
let owner_user_id = maybe_authenticated
|
let owner_user_id = principal.subject().to_string();
|
||||||
.as_ref()
|
|
||||||
.map(|Extension(authenticated)| authenticated.claims().user_id().to_string())
|
|
||||||
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
|
|
||||||
let run = state
|
let run = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.restart_jump_hop_run(run_id, owner_user_id, payload)
|
.restart_jump_hop_run(run_id, owner_user_id, payload)
|
||||||
@@ -711,16 +702,11 @@ async fn persist_jump_hop_generated_image_asset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_jump_hop_work_play_tracking_draft(
|
fn build_jump_hop_work_play_tracking_draft(
|
||||||
authenticated: Option<&AuthenticatedAccessToken>,
|
principal: &RuntimePrincipal,
|
||||||
work_id: impl Into<String>,
|
work_id: impl Into<String>,
|
||||||
source_route: &'static str,
|
source_route: &'static str,
|
||||||
) -> WorkPlayTrackingDraft {
|
) -> WorkPlayTrackingDraft {
|
||||||
match authenticated {
|
WorkPlayTrackingDraft::runtime_principal("jump-hop", work_id, principal, source_route)
|
||||||
Some(authenticated) => {
|
|
||||||
WorkPlayTrackingDraft::new("jump-hop", work_id, authenticated, source_route)
|
|
||||||
}
|
|
||||||
None => WorkPlayTrackingDraft::anonymous("jump-hop", work_id, source_route),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{attach_refresh_session_token, require_bearer_auth},
|
auth::{attach_refresh_session_token, issue_runtime_guest_token, require_bearer_auth},
|
||||||
auth_me::auth_me,
|
auth_me::auth_me,
|
||||||
auth_public_user::{get_public_user_by_code, get_public_user_by_id},
|
auth_public_user::{get_public_user_by_code, get_public_user_by_id},
|
||||||
auth_sessions::{auth_sessions, revoke_auth_session},
|
auth_sessions::{auth_sessions, revoke_auth_session},
|
||||||
@@ -65,6 +65,7 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
attach_refresh_session_token,
|
attach_refresh_session_token,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route("/api/auth/runtime-guest-token", post(issue_runtime_guest_token))
|
||||||
.route("/api/auth/phone/send-code", post(send_phone_code))
|
.route("/api/auth/phone/send-code", post(send_phone_code))
|
||||||
.route("/api/auth/phone/login", post(phone_login))
|
.route("/api/auth/phone/login", post(phone_login))
|
||||||
.route("/api/auth/wechat/start", get(start_wechat_login))
|
.route("/api/auth/wechat/start", get(start_wechat_login))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{attach_optional_bearer_auth, require_bearer_auth},
|
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||||
jump_hop::{
|
jump_hop::{
|
||||||
create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail,
|
create_jump_hop_session, execute_jump_hop_action, get_jump_hop_gallery_detail,
|
||||||
get_jump_hop_runtime_work, get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery,
|
get_jump_hop_runtime_work, get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery,
|
||||||
@@ -58,21 +58,21 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
"/api/runtime/jump-hop/runs",
|
"/api/runtime/jump-hop/runs",
|
||||||
post(start_jump_hop_run).route_layer(middleware::from_fn_with_state(
|
post(start_jump_hop_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
attach_optional_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/jump-hop/runs/{run_id}/jump",
|
"/api/runtime/jump-hop/runs/{run_id}/jump",
|
||||||
post(jump_hop_run_jump).route_layer(middleware::from_fn_with_state(
|
post(jump_hop_run_jump).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
attach_optional_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/jump-hop/runs/{run_id}/restart",
|
"/api/runtime/jump-hop/runs/{run_id}/restart",
|
||||||
post(restart_jump_hop_run).route_layer(middleware::from_fn_with_state(
|
post(restart_jump_hop_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
attach_optional_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route("/api/runtime/jump-hop/gallery", get(list_jump_hop_gallery))
|
.route("/api/runtime/jump-hop/gallery", get(list_jump_hop_gallery))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::require_bearer_auth,
|
auth::{require_bearer_auth, require_runtime_principal_auth},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
wooden_fish::{
|
wooden_fish::{
|
||||||
checkpoint_wooden_fish_run, create_wooden_fish_session, execute_wooden_fish_action,
|
checkpoint_wooden_fish_run, create_wooden_fish_session, execute_wooden_fish_action,
|
||||||
@@ -52,21 +52,21 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
"/api/runtime/wooden-fish/runs",
|
"/api/runtime/wooden-fish/runs",
|
||||||
post(start_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
post(start_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/wooden-fish/runs/{run_id}/checkpoint",
|
"/api/runtime/wooden-fish/runs/{run_id}/checkpoint",
|
||||||
post(checkpoint_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
post(checkpoint_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/wooden-fish/runs/{run_id}/finish",
|
"/api/runtime/wooden-fish/runs/{run_id}/finish",
|
||||||
post(finish_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
post(finish_wooden_fish_run).route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
require_bearer_auth,
|
require_runtime_principal_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ use crate::generated_image_assets::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
auth::AuthenticatedAccessToken,
|
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
openai_image_generation::{
|
openai_image_generation::{
|
||||||
DownloadedOpenAiImage, OpenAiReferenceImage, build_openai_image_http_client,
|
DownloadedOpenAiImage, OpenAiReferenceImage, build_openai_image_http_client,
|
||||||
@@ -229,14 +229,14 @@ pub async fn get_wooden_fish_runtime_work(
|
|||||||
pub async fn start_wooden_fish_run(
|
pub async fn start_wooden_fish_run(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<WoodenFishStartRunRequest>, JsonRejection>,
|
payload: Result<Json<WoodenFishStartRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_RUNTIME_PROVIDER)?;
|
let Json(payload) = wooden_fish_json(payload, &request_context, WOODEN_FISH_RUNTIME_PROVIDER)?;
|
||||||
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
|
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
|
||||||
let run = state
|
let run = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.start_wooden_fish_run(payload, authenticated.claims().user_id().to_string())
|
.start_wooden_fish_run(payload, principal.subject().to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
wooden_fish_error_response(
|
wooden_fish_error_response(
|
||||||
@@ -256,7 +256,7 @@ pub async fn checkpoint_wooden_fish_run(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(run_id): Path<String>,
|
Path(run_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<WoodenFishCheckpointRunRequest>, JsonRejection>,
|
payload: Result<Json<WoodenFishCheckpointRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||||
@@ -265,7 +265,7 @@ pub async fn checkpoint_wooden_fish_run(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.checkpoint_wooden_fish_run(
|
.checkpoint_wooden_fish_run(
|
||||||
run_id,
|
run_id,
|
||||||
authenticated.claims().user_id().to_string(),
|
principal.subject().to_string(),
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -287,7 +287,7 @@ pub async fn finish_wooden_fish_run(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(run_id): Path<String>,
|
Path(run_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(principal): Extension<RuntimePrincipal>,
|
||||||
payload: Result<Json<WoodenFishFinishRunRequest>, JsonRejection>,
|
payload: Result<Json<WoodenFishFinishRunRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||||
@@ -296,7 +296,7 @@ pub async fn finish_wooden_fish_run(
|
|||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.finish_wooden_fish_run(
|
.finish_wooden_fish_run(
|
||||||
run_id,
|
run_id,
|
||||||
authenticated.claims().user_id().to_string(),
|
principal.subject().to_string(),
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use module_runtime::RuntimeTrackingScopeKind;
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::AuthenticatedAccessToken,
|
auth::{AuthenticatedAccessToken, RuntimePrincipal},
|
||||||
request_context::RequestContext,
|
request_context::RequestContext,
|
||||||
state::{AppState, PuzzleApiState},
|
state::{AppState, PuzzleApiState},
|
||||||
tracking::{TrackingEventDraft, record_tracking_event_after_success},
|
tracking::{TrackingEventDraft, record_tracking_event_after_success},
|
||||||
@@ -36,12 +36,28 @@ impl WorkPlayTrackingDraft {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn anonymous(
|
pub(crate) fn runtime_principal(
|
||||||
play_type: &'static str,
|
play_type: &'static str,
|
||||||
work_id: impl Into<String>,
|
work_id: impl Into<String>,
|
||||||
|
principal: &RuntimePrincipal,
|
||||||
source_route: &'static str,
|
source_route: &'static str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::with_user_id(play_type, work_id, None, source_route)
|
match principal {
|
||||||
|
RuntimePrincipal::User(authenticated) => {
|
||||||
|
Self::new(play_type, work_id, authenticated, source_route)
|
||||||
|
}
|
||||||
|
RuntimePrincipal::Guest(claims) => Self::with_user_id(
|
||||||
|
play_type,
|
||||||
|
work_id,
|
||||||
|
Some(claims.subject().to_string()),
|
||||||
|
source_route,
|
||||||
|
)
|
||||||
|
.extra(json!({
|
||||||
|
"principalKind": "guest",
|
||||||
|
"guestSubject": claims.subject(),
|
||||||
|
"guestScope": claims.scope(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_user_id(
|
fn with_user_id(
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ use url::Url;
|
|||||||
|
|
||||||
pub const ACCESS_TOKEN_ALGORITHM: Algorithm = Algorithm::HS256;
|
pub const ACCESS_TOKEN_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||||
pub const DEFAULT_ACCESS_TOKEN_TTL_SECONDS: u64 = 2 * 60 * 60;
|
pub const DEFAULT_ACCESS_TOKEN_TTL_SECONDS: u64 = 2 * 60 * 60;
|
||||||
|
pub const DEFAULT_RUNTIME_GUEST_TOKEN_TTL_SECONDS: u64 = 15 * 60;
|
||||||
|
pub const RUNTIME_GUEST_TOKEN_TYPE: &str = "runtime_guest";
|
||||||
|
pub const RUNTIME_GUEST_SCOPE_PUBLIC_PLAY: &str = "runtime:public-play";
|
||||||
pub const DEFAULT_REFRESH_COOKIE_NAME: &str = "genarrative_refresh_session";
|
pub const DEFAULT_REFRESH_COOKIE_NAME: &str = "genarrative_refresh_session";
|
||||||
pub const DEFAULT_REFRESH_COOKIE_PATH: &str = "/api/auth";
|
pub const DEFAULT_REFRESH_COOKIE_PATH: &str = "/api/auth";
|
||||||
pub const DEFAULT_REFRESH_SESSION_TTL_DAYS: u32 = 30;
|
pub const DEFAULT_REFRESH_SESSION_TTL_DAYS: u32 = 30;
|
||||||
@@ -107,6 +110,21 @@ pub struct AccessTokenClaims {
|
|||||||
pub exp: u64,
|
pub exp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RuntimeGuestTokenClaimsInput {
|
||||||
|
pub subject: String,
|
||||||
|
pub scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeGuestTokenClaims {
|
||||||
|
pub iss: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub typ: String,
|
||||||
|
pub scope: String,
|
||||||
|
pub iat: u64,
|
||||||
|
pub exp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
// 统一承载 JWT 配置,避免 secret、issuer、ttl 在 api-server 与后续模块里散落。
|
// 统一承载 JWT 配置,避免 secret、issuer、ttl 在 api-server 与后续模块里散落。
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct JwtConfig {
|
pub struct JwtConfig {
|
||||||
@@ -417,6 +435,10 @@ impl JwtConfig {
|
|||||||
pub fn access_token_ttl_seconds(&self) -> u64 {
|
pub fn access_token_ttl_seconds(&self) -> u64 {
|
||||||
self.access_token_ttl_seconds
|
self.access_token_ttl_seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runtime_guest_token_ttl_seconds(&self) -> u64 {
|
||||||
|
DEFAULT_RUNTIME_GUEST_TOKEN_TTL_SECONDS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefreshCookieSameSite {
|
impl RefreshCookieSameSite {
|
||||||
@@ -1474,6 +1496,74 @@ impl AccessTokenClaims {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RuntimeGuestTokenClaims {
|
||||||
|
pub fn from_input(
|
||||||
|
input: RuntimeGuestTokenClaimsInput,
|
||||||
|
config: &JwtConfig,
|
||||||
|
issued_at: OffsetDateTime,
|
||||||
|
) -> Result<Self, JwtError> {
|
||||||
|
let subject = normalize_required_field(input.subject, "runtime guest JWT sub 不能为空")?;
|
||||||
|
let scope = normalize_required_field(input.scope, "runtime guest JWT scope 不能为空")?;
|
||||||
|
|
||||||
|
let issued_at_unix = issued_at.unix_timestamp();
|
||||||
|
if issued_at_unix < 0 {
|
||||||
|
return Err(JwtError::InvalidClaims("runtime guest JWT iat 不能早于 Unix epoch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let expires_at = issued_at
|
||||||
|
.checked_add(Duration::seconds(
|
||||||
|
i64::try_from(config.runtime_guest_token_ttl_seconds()).map_err(|_| {
|
||||||
|
JwtError::InvalidConfig("runtime guest JWT 过期时间超出 i64 上限")
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
.ok_or(JwtError::InvalidConfig("runtime guest JWT 过期时间计算溢出"))?;
|
||||||
|
let expires_at_unix = expires_at.unix_timestamp();
|
||||||
|
if expires_at_unix <= issued_at_unix {
|
||||||
|
return Err(JwtError::InvalidClaims("runtime guest JWT exp 必须晚于 iat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let claims = Self {
|
||||||
|
iss: config.issuer().to_string(),
|
||||||
|
sub: subject,
|
||||||
|
typ: RUNTIME_GUEST_TOKEN_TYPE.to_string(),
|
||||||
|
scope,
|
||||||
|
iat: issued_at_unix as u64,
|
||||||
|
exp: expires_at_unix as u64,
|
||||||
|
};
|
||||||
|
claims.validate_for_config(config)?;
|
||||||
|
Ok(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subject(&self) -> &str {
|
||||||
|
&self.sub
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scope(&self) -> &str {
|
||||||
|
&self.scope
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expires_at_unix(&self) -> u64 {
|
||||||
|
self.exp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_for_config(&self, config: &JwtConfig) -> Result<(), JwtError> {
|
||||||
|
if self.iss.trim() != config.issuer() {
|
||||||
|
return Err(JwtError::InvalidClaims(
|
||||||
|
"runtime guest JWT iss 与当前配置不一致",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
normalize_required_field(self.sub.clone(), "runtime guest JWT sub 不能为空")?;
|
||||||
|
normalize_required_field(self.scope.clone(), "runtime guest JWT scope 不能为空")?;
|
||||||
|
if self.typ.trim() != RUNTIME_GUEST_TOKEN_TYPE {
|
||||||
|
return Err(JwtError::InvalidClaims("runtime guest JWT typ 非法"));
|
||||||
|
}
|
||||||
|
if self.exp <= self.iat {
|
||||||
|
return Err(JwtError::InvalidClaims("runtime guest JWT exp 必须晚于 iat"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AccessTokenDeviceInfo {
|
impl AccessTokenDeviceInfo {
|
||||||
pub fn normalize(self) -> Result<Self, JwtError> {
|
pub fn normalize(self) -> Result<Self, JwtError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -1526,6 +1616,26 @@ pub fn sign_access_token(
|
|||||||
.map_err(|error| JwtError::SignFailed(format!("JWT 签发失败:{error}")))
|
.map_err(|error| JwtError::SignFailed(format!("JWT 签发失败:{error}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sign_runtime_guest_token(
|
||||||
|
claims: &RuntimeGuestTokenClaims,
|
||||||
|
config: &JwtConfig,
|
||||||
|
) -> Result<String, JwtError> {
|
||||||
|
claims.validate_for_config(config)?;
|
||||||
|
|
||||||
|
let header = Header {
|
||||||
|
alg: ACCESS_TOKEN_ALGORITHM,
|
||||||
|
typ: Some("JWT".to_string()),
|
||||||
|
..Header::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
encode(
|
||||||
|
&header,
|
||||||
|
claims,
|
||||||
|
&EncodingKey::from_secret(config.secret.as_bytes()),
|
||||||
|
)
|
||||||
|
.map_err(|error| JwtError::SignFailed(format!("runtime guest JWT 签发失败:{error}")))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verify_access_token(token: &str, config: &JwtConfig) -> Result<AccessTokenClaims, JwtError> {
|
pub fn verify_access_token(token: &str, config: &JwtConfig) -> Result<AccessTokenClaims, JwtError> {
|
||||||
let token = token.trim();
|
let token = token.trim();
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
@@ -1552,6 +1662,35 @@ pub fn verify_access_token(token: &str, config: &JwtConfig) -> Result<AccessToke
|
|||||||
Ok(decoded.claims)
|
Ok(decoded.claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn verify_runtime_guest_token(
|
||||||
|
token: &str,
|
||||||
|
config: &JwtConfig,
|
||||||
|
) -> Result<RuntimeGuestTokenClaims, JwtError> {
|
||||||
|
let token = token.trim();
|
||||||
|
if token.is_empty() {
|
||||||
|
return Err(JwtError::VerifyFailed("runtime guest JWT 不能为空".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut validation = Validation::new(ACCESS_TOKEN_ALGORITHM);
|
||||||
|
validation.required_spec_claims = HashSet::from([
|
||||||
|
"exp".to_string(),
|
||||||
|
"iat".to_string(),
|
||||||
|
"iss".to_string(),
|
||||||
|
"sub".to_string(),
|
||||||
|
]);
|
||||||
|
validation.set_issuer(&[config.issuer()]);
|
||||||
|
|
||||||
|
let decoded = decode::<RuntimeGuestTokenClaims>(
|
||||||
|
token,
|
||||||
|
&DecodingKey::from_secret(config.secret.as_bytes()),
|
||||||
|
&validation,
|
||||||
|
)
|
||||||
|
.map_err(map_verify_error)?;
|
||||||
|
|
||||||
|
decoded.claims.validate_for_config(config)?;
|
||||||
|
Ok(decoded.claims)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_refresh_session_token(
|
pub fn read_refresh_session_token(
|
||||||
cookie_header: &str,
|
cookie_header: &str,
|
||||||
config: &RefreshCookieConfig,
|
config: &RefreshCookieConfig,
|
||||||
@@ -2218,6 +2357,30 @@ mod tests {
|
|||||||
.expect("real aliyun sms config should be valid")
|
.expect("real aliyun sms config should be valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trip_sign_and_verify_runtime_guest_token() {
|
||||||
|
let config = build_jwt_config();
|
||||||
|
let issued_at = OffsetDateTime::now_utc();
|
||||||
|
let claims = RuntimeGuestTokenClaims::from_input(
|
||||||
|
RuntimeGuestTokenClaimsInput {
|
||||||
|
subject: "guest-runtime-123".to_string(),
|
||||||
|
scope: RUNTIME_GUEST_SCOPE_PUBLIC_PLAY.to_string(),
|
||||||
|
},
|
||||||
|
&config,
|
||||||
|
issued_at,
|
||||||
|
)
|
||||||
|
.expect("runtime guest claims should build");
|
||||||
|
|
||||||
|
let token = sign_runtime_guest_token(&claims, &config).expect("token should sign");
|
||||||
|
let verified = verify_runtime_guest_token(&token, &config).expect("token should verify");
|
||||||
|
|
||||||
|
assert_eq!(verified, claims);
|
||||||
|
assert_eq!(verified.subject(), "guest-runtime-123");
|
||||||
|
assert_eq!(verified.scope(), RUNTIME_GUEST_SCOPE_PUBLIC_PLAY);
|
||||||
|
assert_eq!(verified.typ, RUNTIME_GUEST_TOKEN_TYPE);
|
||||||
|
assert_eq!(verified.expires_at_unix() - verified.iat, DEFAULT_RUNTIME_GUEST_TOKEN_TTL_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trip_sign_and_verify_access_token() {
|
fn round_trip_sign_and_verify_access_token() {
|
||||||
let config = build_jwt_config();
|
let config = build_jwt_config();
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ pub struct PublicUserSearchResponse {
|
|||||||
pub user: PublicUserSummaryPayload,
|
pub user: PublicUserSummaryPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RuntimeGuestTokenResponse {
|
||||||
|
pub token: String,
|
||||||
|
pub expires_at: String,
|
||||||
|
pub subject: String,
|
||||||
|
pub scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PasswordEntryRequest {
|
pub struct PasswordEntryRequest {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import {
|
|||||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||||
} from '../../services/apiClient';
|
} from '../../services/apiClient';
|
||||||
import {
|
import {
|
||||||
|
ensureRuntimeGuestToken,
|
||||||
getPublicAuthUserByCode,
|
getPublicAuthUserByCode,
|
||||||
getPublicAuthUserById,
|
getPublicAuthUserById,
|
||||||
} from '../../services/authService';
|
} from '../../services/authService';
|
||||||
@@ -127,6 +128,7 @@ import {
|
|||||||
publishBarkBattleWork,
|
publishBarkBattleWork,
|
||||||
updateBarkBattleDraftConfig,
|
updateBarkBattleDraftConfig,
|
||||||
} from '../../services/bark-battle-creation';
|
} from '../../services/bark-battle-creation';
|
||||||
|
import { startBarkBattleRun } from '../../services/bark-battle-runtime';
|
||||||
import {
|
import {
|
||||||
createBigFishCreationSession,
|
createBigFishCreationSession,
|
||||||
executeBigFishCreationAction,
|
executeBigFishCreationAction,
|
||||||
@@ -550,8 +552,13 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
|||||||
]);
|
]);
|
||||||
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
|
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
|
||||||
BACKGROUND_AUTH_REQUEST_OPTIONS;
|
BACKGROUND_AUTH_REQUEST_OPTIONS;
|
||||||
const PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS =
|
async function buildRecommendRuntimeGuestOptions() {
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
const { token } = await ensureRuntimeGuestToken();
|
||||||
|
return {
|
||||||
|
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||||
|
runtimeGuestToken: token,
|
||||||
|
};
|
||||||
|
}
|
||||||
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
||||||
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
||||||
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
||||||
@@ -3253,6 +3260,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
resolveRpgCreationErrorMessage(error, fallback),
|
resolveRpgCreationErrorMessage(error, fallback),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const resolveBarkBattleErrorMessage = useCallback(
|
||||||
|
(error: unknown, fallback: string) =>
|
||||||
|
resolveRpgCreationErrorMessage(error, fallback),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const refreshBigFishShelf = useCallback(async () => {
|
const refreshBigFishShelf = useCallback(async () => {
|
||||||
setIsBigFishLoadingLibrary(true);
|
setIsBigFishLoadingLibrary(true);
|
||||||
@@ -7135,11 +7147,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
profileId: targetProfileId,
|
profileId: targetProfileId,
|
||||||
mode: 'play' as const,
|
mode: 'play' as const,
|
||||||
};
|
};
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startVisualNovelRun(
|
? await startVisualNovelRun(
|
||||||
targetProfileId,
|
targetProfileId,
|
||||||
startRunPayload,
|
startRunPayload,
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
runtimeGuestOptions,
|
||||||
)
|
)
|
||||||
: await startVisualNovelRun(targetProfileId, startRunPayload);
|
: await startVisualNovelRun(targetProfileId, startRunPayload);
|
||||||
setVisualNovelWork(workDetail);
|
setVisualNovelWork(workDetail);
|
||||||
@@ -7186,9 +7201,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setVisualNovelError(null);
|
setVisualNovelError(null);
|
||||||
setIsVisualNovelBusy(true);
|
setIsVisualNovelBusy(true);
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions =
|
||||||
|
activeRecommendRuntimeKind === 'visual-novel'
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const nextRun = await streamVisualNovelRuntimeAction(
|
const nextRun = await streamVisualNovelRuntimeAction(
|
||||||
visualNovelRun.runId,
|
visualNovelRun.runId,
|
||||||
payload,
|
payload,
|
||||||
|
runtimeGuestOptions,
|
||||||
);
|
);
|
||||||
setVisualNovelRun(nextRun);
|
setVisualNovelRun(nextRun);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -7200,6 +7220,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
activeRecommendRuntimeKind,
|
||||||
isVisualNovelBusy,
|
isVisualNovelBusy,
|
||||||
resolvePuzzleErrorMessage,
|
resolvePuzzleErrorMessage,
|
||||||
setIsVisualNovelBusy,
|
setIsVisualNovelBusy,
|
||||||
@@ -7608,12 +7629,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setJumpHopError(null);
|
setJumpHopError(null);
|
||||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const [detail, runResponse] = await Promise.all([
|
const [detail, runResponse] = await Promise.all([
|
||||||
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||||
jumpHopClient.startRun(
|
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||||
normalizedProfileId,
|
|
||||||
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
if (detail?.item) {
|
if (detail?.item) {
|
||||||
setJumpHopWork(detail.item);
|
setJumpHopWork(detail.item);
|
||||||
@@ -7902,9 +7923,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setWoodenFishError(null);
|
setWoodenFishError(null);
|
||||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const [detail, runResponse] = await Promise.all([
|
const [detail, runResponse] = await Promise.all([
|
||||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||||
woodenFishClient.startRun(normalizedProfileId),
|
options.embedded
|
||||||
|
? woodenFishClient.startRun(normalizedProfileId, runtimeGuestOptions)
|
||||||
|
: woodenFishClient.startRun(normalizedProfileId),
|
||||||
]);
|
]);
|
||||||
if (detail?.item) {
|
if (detail?.item) {
|
||||||
setWoodenFishWork(detail.item);
|
setWoodenFishWork(detail.item);
|
||||||
@@ -8384,15 +8410,15 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
profileId: item.profileId,
|
profileId: item.profileId,
|
||||||
levelId: levelId ?? null,
|
levelId: levelId ?? null,
|
||||||
};
|
};
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const authMode = options.embedded
|
const authMode = options.embedded
|
||||||
? 'isolated'
|
? 'isolated'
|
||||||
: (options.authMode ?? 'default');
|
: (options.authMode ?? 'default');
|
||||||
const { run } =
|
const { run } =
|
||||||
authMode === 'isolated'
|
authMode === 'isolated'
|
||||||
? await startPuzzleRun(
|
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||||
startRunPayload,
|
|
||||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
|
||||||
)
|
|
||||||
: await startPuzzleRun(startRunPayload);
|
: await startPuzzleRun(startRunPayload);
|
||||||
setSelectedPuzzleDetail(item);
|
setSelectedPuzzleDetail(item);
|
||||||
setPuzzleRun(run);
|
setPuzzleRun(run);
|
||||||
@@ -8488,10 +8514,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
runtimeProfile.generatedBackgroundAsset,
|
runtimeProfile.generatedBackgroundAsset,
|
||||||
{ expireSeconds: 300 },
|
{ expireSeconds: 300 },
|
||||||
);
|
);
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const runtimeOptions = {
|
const runtimeOptions = {
|
||||||
...(options.embedded
|
...runtimeGuestOptions,
|
||||||
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
|
|
||||||
: {}),
|
|
||||||
...(typeof options.itemTypeCountOverride === 'number'
|
...(typeof options.itemTypeCountOverride === 'number'
|
||||||
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -8559,11 +8586,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setSquareHoleError(null);
|
setSquareHoleError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startSquareHoleRun(
|
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||||
profile.profileId,
|
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
|
||||||
)
|
|
||||||
: await startSquareHoleRun(profile.profileId);
|
: await startSquareHoleRun(profile.profileId);
|
||||||
setSquareHoleRun(run);
|
setSquareHoleRun(run);
|
||||||
setSquareHoleRuntimeReturnStage(returnStage);
|
setSquareHoleRuntimeReturnStage(returnStage);
|
||||||
@@ -8715,9 +8742,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
bigFishInputInFlightRef.current = true;
|
bigFishInputInFlightRef.current = true;
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions =
|
||||||
|
activeRecommendRuntimeKind === 'big-fish'
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const { run } = await submitBigFishRuntimeInput(
|
const { run } = await submitBigFishRuntimeInput(
|
||||||
bigFishRun.runId,
|
bigFishRun.runId,
|
||||||
payload,
|
payload,
|
||||||
|
runtimeGuestOptions,
|
||||||
);
|
);
|
||||||
setBigFishRun(run);
|
setBigFishRun(run);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -8728,7 +8760,12 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
bigFishInputInFlightRef.current = false;
|
bigFishInputInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[bigFishRun, resolveBigFishErrorMessage, setBigFishError],
|
[
|
||||||
|
activeRecommendRuntimeKind,
|
||||||
|
bigFishRun,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
setBigFishError,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reportBigFishObservedPlayTime = useCallback(() => {
|
const reportBigFishObservedPlayTime = useCallback(() => {
|
||||||
@@ -8929,12 +8966,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
profileId: currentLevel.profileId,
|
profileId: currentLevel.profileId,
|
||||||
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
||||||
};
|
};
|
||||||
|
const runtimeGuestOptions =
|
||||||
|
puzzleRuntimeAuthMode === 'isolated'
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const { run } =
|
const { run } =
|
||||||
puzzleRuntimeAuthMode === 'isolated'
|
puzzleRuntimeAuthMode === 'isolated'
|
||||||
? await startPuzzleRun(
|
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||||
startRunPayload,
|
|
||||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
|
||||||
)
|
|
||||||
: await startPuzzleRun(startRunPayload);
|
: await startPuzzleRun(startRunPayload);
|
||||||
setSelectedPuzzleDetail(detailItem);
|
setSelectedPuzzleDetail(detailItem);
|
||||||
puzzleRunRef.current = run;
|
puzzleRunRef.current = run;
|
||||||
@@ -9057,10 +9095,8 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const submitLeaderboardPromise =
|
const submitLeaderboardPromise =
|
||||||
puzzleRuntimeAuthMode === 'isolated'
|
puzzleRuntimeAuthMode === 'isolated'
|
||||||
? submitPuzzleLeaderboard(
|
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
|
||||||
puzzleRun.runId,
|
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeGuestOptions),
|
||||||
payload,
|
|
||||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
|
||||||
)
|
)
|
||||||
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
|
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
|
||||||
|
|
||||||
@@ -9117,6 +9153,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runtimeGuestOptions =
|
||||||
|
puzzleRuntimeAuthMode === 'isolated'
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const targetProfileId = _target?.profileId?.trim() ?? '';
|
const targetProfileId = _target?.profileId?.trim() ?? '';
|
||||||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||||||
const itemPromise =
|
const itemPromise =
|
||||||
@@ -9132,7 +9172,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
{
|
{
|
||||||
targetProfileId,
|
targetProfileId,
|
||||||
},
|
},
|
||||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
runtimeGuestOptions,
|
||||||
)
|
)
|
||||||
: advancePuzzleNextLevel(puzzleRun.runId, {
|
: advancePuzzleNextLevel(puzzleRun.runId, {
|
||||||
targetProfileId,
|
targetProfileId,
|
||||||
@@ -9157,7 +9197,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
? await advancePuzzleNextLevel(
|
? await advancePuzzleNextLevel(
|
||||||
puzzleRun.runId,
|
puzzleRun.runId,
|
||||||
{},
|
{},
|
||||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
runtimeGuestOptions,
|
||||||
)
|
)
|
||||||
: await advancePuzzleNextLevel(puzzleRun.runId);
|
: await advancePuzzleNextLevel(puzzleRun.runId);
|
||||||
setPuzzleRun(run);
|
setPuzzleRun(run);
|
||||||
@@ -10993,11 +11033,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishRuntimeReturnStage(returnStage);
|
setBigFishRuntimeReturnStage(returnStage);
|
||||||
setBigFishRun(null);
|
setBigFishRun(null);
|
||||||
try {
|
try {
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startBigFishRuntimeRun(
|
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||||
sessionId,
|
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
|
||||||
)
|
|
||||||
: await startBigFishRuntimeRun(sessionId);
|
: await startBigFishRuntimeRun(sessionId);
|
||||||
setBigFishRuntimeStartedAt(Date.now());
|
setBigFishRuntimeStartedAt(Date.now());
|
||||||
setBigFishRun(run);
|
setBigFishRun(run);
|
||||||
@@ -11008,11 +11048,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const recordPlayPromise = options.embedded
|
const recordPlayPromise = options.embedded
|
||||||
? recordBigFishPlay(
|
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
|
||||||
sessionId,
|
|
||||||
{ elapsedMs: 0 },
|
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
|
||||||
)
|
|
||||||
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
|
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
|
||||||
void recordPlayPromise.catch((error) => {
|
void recordPlayPromise.catch((error) => {
|
||||||
setBigFishError(
|
setBigFishError(
|
||||||
@@ -11031,9 +11067,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const startBarkBattleRunFromWork = useCallback(
|
const startBarkBattleRunFromWork = useCallback(
|
||||||
(
|
async (
|
||||||
item: BarkBattleWorkSummary,
|
item: BarkBattleWorkSummary,
|
||||||
returnStage: BarkBattleRuntimeReturnStage = 'work-detail',
|
returnStage: BarkBattleRuntimeReturnStage = 'work-detail',
|
||||||
|
options: { embedded?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
if (item.status !== 'published') {
|
if (item.status !== 'published') {
|
||||||
setBarkBattleError('汪汪声浪作品发布后才能进入正式玩法。');
|
setBarkBattleError('汪汪声浪作品发布后才能进入正式玩法。');
|
||||||
@@ -11045,7 +11082,16 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBarkBattleRuntimeMode('published');
|
setBarkBattleRuntimeMode('published');
|
||||||
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
||||||
setBarkBattleRuntimeReturnStage(returnStage);
|
setBarkBattleRuntimeReturnStage(returnStage);
|
||||||
|
try {
|
||||||
|
const runtimeGuestOptions = options.embedded
|
||||||
|
? await buildRecommendRuntimeGuestOptions()
|
||||||
|
: {};
|
||||||
|
const runResponse = options.embedded
|
||||||
|
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||||
|
: await startBarkBattleRun(item.workId);
|
||||||
|
void runResponse;
|
||||||
selectionStageRef.current = 'bark-battle-runtime';
|
selectionStageRef.current = 'bark-battle-runtime';
|
||||||
|
if (!options.embedded) {
|
||||||
setSelectionStage('bark-battle-runtime');
|
setSelectionStage('bark-battle-runtime');
|
||||||
pushAppHistoryPath(
|
pushAppHistoryPath(
|
||||||
buildPublicWorkStagePath(
|
buildPublicWorkStagePath(
|
||||||
@@ -11053,9 +11099,16 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
buildBarkBattlePublicWorkCode(item.workId),
|
buildBarkBattlePublicWorkCode(item.workId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setBarkBattleError(
|
||||||
|
resolveBarkBattleErrorMessage(error, '启动汪汪声浪玩法失败。'),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setSelectionStage],
|
[resolveBarkBattleErrorMessage, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startSelectedPublicWork = useCallback(() => {
|
const startSelectedPublicWork = useCallback(() => {
|
||||||
@@ -11327,7 +11380,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
'当前汪汪声浪作品信息不完整,暂时无法进入玩法。',
|
'当前汪汪声浪作品信息不完整,暂时无法进入玩法。',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
started = startBarkBattleRunFromWork(work, 'platform');
|
started = await startBarkBattleRunFromWork(work, 'platform', {
|
||||||
|
embedded: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (isEdutainmentGalleryEntry(entry)) {
|
} else if (isEdutainmentGalleryEntry(entry)) {
|
||||||
started = await startBabyObjectMatchRuntimeFromEntry(
|
started = await startBabyObjectMatchRuntimeFromEntry(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import type {
|
|||||||
AuthWechatStartResponse,
|
AuthWechatStartResponse,
|
||||||
LogoutResponse,
|
LogoutResponse,
|
||||||
PublicUserSearchResponse,
|
PublicUserSearchResponse,
|
||||||
|
RuntimeGuestTokenResponse,
|
||||||
} from '../../packages/shared/src/contracts/auth';
|
} from '../../packages/shared/src/contracts/auth';
|
||||||
import type { RedeemProfileReferralInviteCodeResponse } from '../../packages/shared/src/contracts/runtime';
|
import type { RedeemProfileReferralInviteCodeResponse } from '../../packages/shared/src/contracts/runtime';
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,42 @@ const PUBLIC_AUTH_REQUEST_OPTIONS = {
|
|||||||
skipRefresh: true,
|
skipRefresh: true,
|
||||||
} satisfies ApiRequestOptions;
|
} satisfies ApiRequestOptions;
|
||||||
|
|
||||||
|
const runtimeGuestTokenCache: {
|
||||||
|
value: RuntimeGuestTokenResponse | null;
|
||||||
|
} = {
|
||||||
|
value: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isRuntimeGuestTokenFresh(response: RuntimeGuestTokenResponse | null) {
|
||||||
|
if (!response?.expiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expiresAtMs = Date.parse(response.expiresAt);
|
||||||
|
return Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > 15_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRuntimeGuestTokenCache() {
|
||||||
|
runtimeGuestTokenCache.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureRuntimeGuestToken() {
|
||||||
|
if (isRuntimeGuestTokenFresh(runtimeGuestTokenCache.value)) {
|
||||||
|
return runtimeGuestTokenCache.value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await requestJson<RuntimeGuestTokenResponse>(
|
||||||
|
'/api/auth/runtime-guest-token',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
'获取匿名运行态身份失败',
|
||||||
|
PUBLIC_AUTH_REQUEST_OPTIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
runtimeGuestTokenCache.value = response;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
const LAST_LOGIN_PHONE_STORAGE_KEY = 'genarrative:last-login-phone';
|
const LAST_LOGIN_PHONE_STORAGE_KEY = 'genarrative:last-login-phone';
|
||||||
|
|
||||||
export function normalizePhoneInput(phoneInput: string) {
|
export function normalizePhoneInput(phoneInput: string) {
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import type {
|
|||||||
BarkBattleRuntimeConfig,
|
BarkBattleRuntimeConfig,
|
||||||
} from '../../../packages/shared/src/contracts/barkBattle';
|
} from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const BARK_BATTLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const BARK_BATTLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
@@ -24,28 +28,20 @@ const BARK_BATTLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BarkBattleRuntimeRequestOptions = Pick<
|
export type BarkBattleRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export function getBarkBattleRuntimeConfig(
|
export function getBarkBattleRuntimeConfig(
|
||||||
workId: string,
|
workId: string,
|
||||||
options: BarkBattleRuntimeRequestOptions = {},
|
options: BarkBattleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BarkBattleRuntimeConfig>(
|
return requestJson<BarkBattleRuntimeConfig>(
|
||||||
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/config`,
|
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/config`,
|
||||||
{ method: 'GET' },
|
{ method: 'GET', headers: buildRuntimeGuestHeaders(options) },
|
||||||
'读取汪汪声浪大作战配置失败',
|
'读取汪汪声浪大作战配置失败',
|
||||||
{
|
{
|
||||||
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,11 +51,12 @@ export function startBarkBattleRun(
|
|||||||
payload: Partial<BarkBattleRunStartRequest> = {},
|
payload: Partial<BarkBattleRunStartRequest> = {},
|
||||||
options: BarkBattleRuntimeRequestOptions = {},
|
options: BarkBattleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BarkBattleRunStartResponse>(
|
return requestJson<BarkBattleRunStartResponse>(
|
||||||
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/runs`,
|
`/api/runtime/bark-battle/works/${encodeURIComponent(workId)}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, { 'Content-Type': 'application/json' }),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...payload,
|
...payload,
|
||||||
workId: payload.workId ?? workId,
|
workId: payload.workId ?? workId,
|
||||||
@@ -68,10 +65,7 @@ export function startBarkBattleRun(
|
|||||||
'启动汪汪声浪大作战正式局失败',
|
'启动汪汪声浪大作战正式局失败',
|
||||||
{
|
{
|
||||||
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,16 +74,14 @@ export function getBarkBattleRun(
|
|||||||
runId: string,
|
runId: string,
|
||||||
options: BarkBattleRuntimeRequestOptions = {},
|
options: BarkBattleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<unknown>(
|
return requestJson<unknown>(
|
||||||
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}`,
|
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}`,
|
||||||
{ method: 'GET' },
|
{ method: 'GET', headers: buildRuntimeGuestHeaders(options) },
|
||||||
'读取汪汪声浪大作战单局失败',
|
'读取汪汪声浪大作战单局失败',
|
||||||
{
|
{
|
||||||
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
retry: BARK_BATTLE_RUNTIME_READ_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -99,11 +91,12 @@ export function finishBarkBattleRun(
|
|||||||
payload: BarkBattleRunFinishRequest,
|
payload: BarkBattleRunFinishRequest,
|
||||||
options: BarkBattleRuntimeRequestOptions = {},
|
options: BarkBattleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BarkBattleFinishResponse>(
|
return requestJson<BarkBattleFinishResponse>(
|
||||||
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}/finish`,
|
`/api/runtime/bark-battle/runs/${encodeURIComponent(runId)}/finish`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, { 'Content-Type': 'application/json' }),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...payload,
|
...payload,
|
||||||
runId: payload.runId ?? runId,
|
runId: payload.runId ?? runId,
|
||||||
@@ -112,10 +105,7 @@ export function finishBarkBattleRun(
|
|||||||
'提交汪汪声浪大作战成绩失败',
|
'提交汪汪声浪大作战成绩失败',
|
||||||
{
|
{
|
||||||
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
retry: BARK_BATTLE_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import type {
|
|||||||
} from '../../../packages/shared/src/contracts/bigFish';
|
} from '../../../packages/shared/src/contracts/bigFish';
|
||||||
import type { BigFishWorksResponse } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
import type { BigFishWorksResponse } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
@@ -16,13 +20,7 @@ const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
type BigFishRuntimeRequestOptions = Pick<
|
type BigFishRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上报大鱼吃小鱼正式游玩。elapsedMs 为 0 时仅标记玩过作品。
|
* 上报大鱼吃小鱼正式游玩。elapsedMs 为 0 时仅标记玩过作品。
|
||||||
@@ -32,20 +30,20 @@ export function recordBigFishPlay(
|
|||||||
payload: RecordBigFishPlayRequest,
|
payload: RecordBigFishPlayRequest,
|
||||||
options: BigFishRuntimeRequestOptions = {},
|
options: BigFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BigFishWorksResponse>(
|
return requestJson<BigFishWorksResponse>(
|
||||||
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/play`,
|
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/play`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
},
|
},
|
||||||
'记录大鱼吃小鱼游玩失败',
|
'记录大鱼吃小鱼游玩失败',
|
||||||
{
|
{
|
||||||
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -54,18 +52,17 @@ export function startBigFishRun(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
options: BigFishRuntimeRequestOptions = {},
|
options: BigFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BigFishRunResponse>(
|
return requestJson<BigFishRunResponse>(
|
||||||
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/runs`,
|
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
'启动大鱼吃小鱼玩法失败',
|
'启动大鱼吃小鱼玩法失败',
|
||||||
{
|
{
|
||||||
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,17 +80,22 @@ export function getBigFishRun(runId: string) {
|
|||||||
export function submitBigFishInput(
|
export function submitBigFishInput(
|
||||||
runId: string,
|
runId: string,
|
||||||
payload: SubmitBigFishInputRequest,
|
payload: SubmitBigFishInputRequest,
|
||||||
|
options: BigFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<BigFishRunResponse>(
|
return requestJson<BigFishRunResponse>(
|
||||||
`/api/runtime/big-fish/runs/${encodeURIComponent(runId)}/input`,
|
`/api/runtime/big-fish/runs/${encodeURIComponent(runId)}/input`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
},
|
},
|
||||||
'同步大鱼吃小鱼输入失败',
|
'同步大鱼吃小鱼输入失败',
|
||||||
{
|
{
|
||||||
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
||||||
|
...requestOptions,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ import type {
|
|||||||
JumpHopWorkSummaryResponse,
|
JumpHopWorkSummaryResponse,
|
||||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
import { createCreationAgentClient } from '../creation-agent';
|
import { createCreationAgentClient } from '../creation-agent';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions';
|
const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions';
|
||||||
const JUMP_HOP_WORKS_API_BASE = '/api/creation/jump-hop/works';
|
const JUMP_HOP_WORKS_API_BASE = '/api/creation/jump-hop/works';
|
||||||
@@ -31,14 +35,7 @@ const JUMP_HOP_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
|||||||
baseDelayMs: 120,
|
baseDelayMs: 120,
|
||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
};
|
};
|
||||||
type JumpHopRuntimeRequestOptions = Pick<
|
type JumpHopRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipAuth'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
JumpHopActionRequest,
|
JumpHopActionRequest,
|
||||||
@@ -237,22 +234,20 @@ export async function startJumpHopRuntimeRun(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
options: JumpHopRuntimeRequestOptions = {},
|
options: JumpHopRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<JumpHopRunResponse>(
|
return requestJson<JumpHopRunResponse>(
|
||||||
`${JUMP_HOP_RUNTIME_API_BASE}/runs`,
|
`${JUMP_HOP_RUNTIME_API_BASE}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ profileId }),
|
body: JSON.stringify({ profileId }),
|
||||||
},
|
},
|
||||||
'启动跳一跳运行态失败',
|
'启动跳一跳运行态失败',
|
||||||
{
|
{
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipAuth: options.skipAuth,
|
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,7 +255,9 @@ export async function startJumpHopRuntimeRun(
|
|||||||
export async function submitJumpHopJump(
|
export async function submitJumpHopJump(
|
||||||
runId: string,
|
runId: string,
|
||||||
payload: { chargeMs: number },
|
payload: { chargeMs: number },
|
||||||
|
options: JumpHopRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
chargeMs: payload.chargeMs,
|
chargeMs: payload.chargeMs,
|
||||||
clientEventId: `jump-${runId}-${Date.now()}`,
|
clientEventId: `jump-${runId}-${Date.now()}`,
|
||||||
@@ -272,26 +269,34 @@ export async function submitJumpHopJump(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestPayload),
|
body: JSON.stringify(requestPayload),
|
||||||
},
|
},
|
||||||
'提交跳一跳起跳失败',
|
'提交跳一跳起跳失败',
|
||||||
|
requestOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restartJumpHopRuntimeRun(runId: string) {
|
export async function restartJumpHopRuntimeRun(
|
||||||
|
runId: string,
|
||||||
|
options: JumpHopRuntimeRequestOptions = {},
|
||||||
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<JumpHopRunResponse>(
|
return requestJson<JumpHopRunResponse>(
|
||||||
`${JUMP_HOP_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/restart`,
|
`${JUMP_HOP_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/restart`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
clientActionId: `restart-${runId}-${Date.now()}`,
|
clientActionId: `restart-${runId}-${Date.now()}`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'重新开始跳一跳失败',
|
'重新开始跳一跳失败',
|
||||||
|
requestOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import type {
|
|||||||
StopMatch3DRunRequest,
|
StopMatch3DRunRequest,
|
||||||
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const MATCH3D_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const MATCH3D_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
@@ -25,13 +29,7 @@ const MATCH3D_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
export type Match3DRuntimeRequestOptions = Pick<
|
export type Match3DRuntimeRequestOptions = RuntimeGuestRequestOptions & {
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
> & {
|
|
||||||
itemTypeCountOverride?: number | null;
|
itemTypeCountOverride?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,6 +74,7 @@ export function startMatch3DRun(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
options: Match3DRuntimeRequestOptions = {},
|
options: Match3DRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
const payload: StartMatch3DRunRequest = {
|
const payload: StartMatch3DRunRequest = {
|
||||||
profileId,
|
profileId,
|
||||||
itemTypeCountOverride: options.itemTypeCountOverride ?? null,
|
itemTypeCountOverride: options.itemTypeCountOverride ?? null,
|
||||||
@@ -85,16 +84,15 @@ export function startMatch3DRun(
|
|||||||
`/api/runtime/match3d/works/${encodeURIComponent(profileId)}/runs`,
|
`/api/runtime/match3d/works/${encodeURIComponent(profileId)}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
},
|
},
|
||||||
'启动抓大鹅玩法失败',
|
'启动抓大鹅玩法失败',
|
||||||
{
|
{
|
||||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import type {
|
|||||||
UsePuzzleRuntimePropRequest,
|
UsePuzzleRuntimePropRequest,
|
||||||
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const PUZZLE_RUNTIME_API_BASE = '/api/runtime/puzzle/runs';
|
const PUZZLE_RUNTIME_API_BASE = '/api/runtime/puzzle/runs';
|
||||||
const PUZZLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const PUZZLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
@@ -26,13 +30,7 @@ const PUZZLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
type PuzzleRuntimeRequestOptions = Pick<
|
type PuzzleRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从某个已发布拼图作品开始一次 run。
|
* 从某个已发布拼图作品开始一次 run。
|
||||||
@@ -41,20 +39,20 @@ export async function startPuzzleRun(
|
|||||||
payload: StartPuzzleRunRequest,
|
payload: StartPuzzleRunRequest,
|
||||||
options: PuzzleRuntimeRequestOptions = {},
|
options: PuzzleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<PuzzleRunResponse>(
|
return requestJson<PuzzleRunResponse>(
|
||||||
PUZZLE_RUNTIME_API_BASE,
|
PUZZLE_RUNTIME_API_BASE,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
},
|
},
|
||||||
'启动拼图玩法失败',
|
'启动拼图玩法失败',
|
||||||
{
|
{
|
||||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,6 +123,7 @@ export async function advancePuzzleNextLevel(
|
|||||||
payload: AdvancePuzzleNextLevelRequest = {},
|
payload: AdvancePuzzleNextLevelRequest = {},
|
||||||
options: PuzzleRuntimeRequestOptions = {},
|
options: PuzzleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
const targetProfileId = payload.targetProfileId?.trim() ?? '';
|
const targetProfileId = payload.targetProfileId?.trim() ?? '';
|
||||||
return requestJson<PuzzleRunResponse>(
|
return requestJson<PuzzleRunResponse>(
|
||||||
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
|
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
|
||||||
@@ -132,18 +131,19 @@ export async function advancePuzzleNextLevel(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
...(targetProfileId
|
...(targetProfileId
|
||||||
? {
|
? {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify({ targetProfileId }),
|
body: JSON.stringify({ targetProfileId }),
|
||||||
}
|
}
|
||||||
: {}),
|
: {
|
||||||
|
headers: buildRuntimeGuestHeaders(options),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
'进入下一关失败',
|
'进入下一关失败',
|
||||||
{
|
{
|
||||||
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
retry: PUZZLE_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/services/recommendedRuntimeGuestLaunch.test.ts
Normal file
113
src/services/recommendedRuntimeGuestLaunch.test.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const apiClientMocks = vi.hoisted(() => ({
|
||||||
|
requestJson: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./apiClient', async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof import('./apiClient')>('./apiClient');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
requestJson: apiClientMocks.requestJson,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { startBigFishRun } from './big-fish-runtime/bigFishRuntimeClient';
|
||||||
|
import { startBarkBattleRun } from './bark-battle-runtime/barkBattleRuntimeClient';
|
||||||
|
import { startJumpHopRuntimeRun } from './jump-hop/jumpHopClient';
|
||||||
|
import { startMatch3DRun } from './match3d-runtime/match3dRuntimeClient';
|
||||||
|
import { startPuzzleRun } from './puzzle-runtime/puzzleRuntimeClient';
|
||||||
|
import { startSquareHoleRun } from './square-hole-runtime/squareHoleRuntimeClient';
|
||||||
|
import { startVisualNovelRun } from './visual-novel-runtime/visualNovelRuntimeClient';
|
||||||
|
|
||||||
|
describe('recommended runtime guest launch clients', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
apiClientMocks.requestJson.mockResolvedValue({ run: {} });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: 'jump-hop',
|
||||||
|
start: () =>
|
||||||
|
startJumpHopRuntimeRun('jump-hop-profile-1', {
|
||||||
|
runtimeGuestToken: 'runtime-guest-token',
|
||||||
|
}),
|
||||||
|
expectedUrl: '/api/runtime/jump-hop/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'visual-novel',
|
||||||
|
start: () =>
|
||||||
|
startVisualNovelRun(
|
||||||
|
'visual-novel-profile-1',
|
||||||
|
{ profileId: 'visual-novel-profile-1', mode: 'play' },
|
||||||
|
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||||
|
),
|
||||||
|
expectedUrl: '/api/runtime/visual-novel/works/visual-novel-profile-1/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'match3d',
|
||||||
|
start: () =>
|
||||||
|
startMatch3DRun('match3d-profile-1', {
|
||||||
|
runtimeGuestToken: 'runtime-guest-token',
|
||||||
|
}),
|
||||||
|
expectedUrl: '/api/runtime/match3d/works/match3d-profile-1/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'square-hole',
|
||||||
|
start: () =>
|
||||||
|
startSquareHoleRun('square-hole-profile-1', {
|
||||||
|
runtimeGuestToken: 'runtime-guest-token',
|
||||||
|
}),
|
||||||
|
expectedUrl: '/api/runtime/square-hole/works/square-hole-profile-1/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'big-fish',
|
||||||
|
start: () =>
|
||||||
|
startBigFishRun('big-fish-session-1', {
|
||||||
|
runtimeGuestToken: 'runtime-guest-token',
|
||||||
|
}),
|
||||||
|
expectedUrl: '/api/runtime/big-fish/sessions/big-fish-session-1/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bark-battle',
|
||||||
|
start: () =>
|
||||||
|
startBarkBattleRun('bark-battle-work-1', {}, {
|
||||||
|
runtimeGuestToken: 'runtime-guest-token',
|
||||||
|
}),
|
||||||
|
expectedUrl: '/api/runtime/bark-battle/works/bark-battle-work-1/runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'puzzle',
|
||||||
|
start: () =>
|
||||||
|
startPuzzleRun(
|
||||||
|
{ profileId: 'puzzle-profile-1', levelId: 'level-1' },
|
||||||
|
{ runtimeGuestToken: 'runtime-guest-token' },
|
||||||
|
),
|
||||||
|
expectedUrl: '/api/runtime/puzzle/runs',
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'$name start request uses the runtime guest bearer token without touching login auth',
|
||||||
|
async ({ start, expectedUrl }) => {
|
||||||
|
await start();
|
||||||
|
|
||||||
|
const [url, init, , options] = apiClientMocks.requestJson.mock.calls[0];
|
||||||
|
expect(url).toBe(expectedUrl);
|
||||||
|
expect(init).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
Authorization: 'Bearer runtime-guest-token',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(options).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
skipAuth: true,
|
||||||
|
skipRefresh: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
40
src/services/runtimeGuestAuth.ts
Normal file
40
src/services/runtimeGuestAuth.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { ApiRequestOptions } from './apiClient';
|
||||||
|
|
||||||
|
export type RuntimeGuestRequestOptions = Pick<
|
||||||
|
ApiRequestOptions,
|
||||||
|
| 'authImpact'
|
||||||
|
| 'skipAuth'
|
||||||
|
| 'skipRefresh'
|
||||||
|
| 'notifyAuthStateChange'
|
||||||
|
| 'clearAuthOnUnauthorized'
|
||||||
|
> & {
|
||||||
|
runtimeGuestToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildRuntimeGuestHeaders(
|
||||||
|
options: Pick<RuntimeGuestRequestOptions, 'runtimeGuestToken'>,
|
||||||
|
headers: Record<string, string> = {},
|
||||||
|
) {
|
||||||
|
const runtimeGuestToken = options.runtimeGuestToken?.trim();
|
||||||
|
if (!runtimeGuestToken) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...headers,
|
||||||
|
Authorization: `Bearer ${runtimeGuestToken}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRuntimeGuestAuthOptions<
|
||||||
|
TOptions extends RuntimeGuestRequestOptions,
|
||||||
|
>(options: TOptions) {
|
||||||
|
const runtimeGuestToken = options.runtimeGuestToken?.trim();
|
||||||
|
return {
|
||||||
|
authImpact: options.authImpact,
|
||||||
|
skipAuth: runtimeGuestToken ? true : options.skipAuth,
|
||||||
|
skipRefresh: runtimeGuestToken ? true : options.skipRefresh,
|
||||||
|
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||||
|
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||||
|
} satisfies ApiRequestOptions;
|
||||||
|
}
|
||||||
@@ -5,10 +5,14 @@ import type {
|
|||||||
StopSquareHoleRunRequest,
|
StopSquareHoleRunRequest,
|
||||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const SQUARE_HOLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const SQUARE_HOLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
maxRetries: 1,
|
maxRetries: 1,
|
||||||
@@ -21,13 +25,7 @@ const SQUARE_HOLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
type SquareHoleRuntimeRequestOptions = Pick<
|
type SquareHoleRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于作品启动一局方洞挑战正式 run。
|
* 基于作品启动一局方洞挑战正式 run。
|
||||||
@@ -36,20 +34,20 @@ export function startSquareHoleRun(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
options: SquareHoleRuntimeRequestOptions = {},
|
options: SquareHoleRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<SquareHoleRunResponse>(
|
return requestJson<SquareHoleRunResponse>(
|
||||||
`/api/runtime/square-hole/works/${encodeURIComponent(profileId)}/runs`,
|
`/api/runtime/square-hole/works/${encodeURIComponent(profileId)}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
body: JSON.stringify({ profileId }),
|
body: JSON.stringify({ profileId }),
|
||||||
},
|
},
|
||||||
'启动方洞挑战失败',
|
'启动方洞挑战失败',
|
||||||
{
|
{
|
||||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,16 @@ import type {
|
|||||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||||
import type { TextStreamOptions } from '../aiTypes';
|
import type { TextStreamOptions } from '../aiTypes';
|
||||||
import {
|
import {
|
||||||
type ApiRequestOptions,
|
|
||||||
type ApiRetryOptions,
|
type ApiRetryOptions,
|
||||||
fetchWithApiAuth,
|
fetchWithApiAuth,
|
||||||
requestJson,
|
requestJson,
|
||||||
} from '../apiClient';
|
} from '../apiClient';
|
||||||
import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse';
|
import { readVisualNovelRuntimeRunFromSse } from './visualNovelRuntimeSse';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const VISUAL_NOVEL_RUNTIME_API_BASE = '/api/runtime/visual-novel';
|
const VISUAL_NOVEL_RUNTIME_API_BASE = '/api/runtime/visual-novel';
|
||||||
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
const VISUAL_NOVEL_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||||
@@ -39,16 +43,11 @@ const VISUAL_NOVEL_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VisualNovelRuntimeStreamOptions = TextStreamOptions & {
|
export type VisualNovelRuntimeStreamOptions = TextStreamOptions &
|
||||||
|
RuntimeGuestRequestOptions & {
|
||||||
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
|
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
|
||||||
};
|
};
|
||||||
type VisualNovelRuntimeRequestOptions = Pick<
|
type VisualNovelRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
ApiRequestOptions,
|
|
||||||
| 'authImpact'
|
|
||||||
| 'skipRefresh'
|
|
||||||
| 'notifyAuthStateChange'
|
|
||||||
| 'clearAuthOnUnauthorized'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type VisualNovelSaveArchiveResumeResponse =
|
export type VisualNovelSaveArchiveResumeResponse =
|
||||||
ProfileSaveArchiveResumeResponse<
|
ProfileSaveArchiveResumeResponse<
|
||||||
@@ -84,11 +83,20 @@ async function openVisualNovelRuntimeSsePost(
|
|||||||
payload: unknown,
|
payload: unknown,
|
||||||
fallbackMessage: string,
|
fallbackMessage: string,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
|
options: RuntimeGuestRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
const response = await fetchWithApiAuth(url, {
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
|
const response = await fetchWithApiAuth(
|
||||||
|
url,
|
||||||
|
{
|
||||||
...buildJsonInit('POST', payload),
|
...buildJsonInit('POST', payload),
|
||||||
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
signal,
|
signal,
|
||||||
});
|
},
|
||||||
|
requestOptions,
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
@@ -107,17 +115,20 @@ export async function startVisualNovelRun(
|
|||||||
payload: VisualNovelStartRunRequest,
|
payload: VisualNovelStartRunRequest,
|
||||||
options: VisualNovelRuntimeRequestOptions = {},
|
options: VisualNovelRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<VisualNovelRunResponse>(
|
return requestJson<VisualNovelRunResponse>(
|
||||||
`${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`,
|
`${VISUAL_NOVEL_RUNTIME_API_BASE}/works/${encodeURIComponent(profileId)}/runs`,
|
||||||
buildJsonInit('POST', payload),
|
{
|
||||||
|
...buildJsonInit('POST', payload),
|
||||||
|
headers: buildRuntimeGuestHeaders(options, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
|
},
|
||||||
'启动视觉小说运行失败',
|
'启动视觉小说运行失败',
|
||||||
{
|
{
|
||||||
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
|
retry: VISUAL_NOVEL_RUNTIME_WRITE_RETRY,
|
||||||
timeoutMs: 15000,
|
timeoutMs: 15000,
|
||||||
authImpact: options.authImpact,
|
...requestOptions,
|
||||||
skipRefresh: options.skipRefresh,
|
|
||||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
|
||||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -154,6 +165,7 @@ export async function streamVisualNovelRuntimeAction(
|
|||||||
payload,
|
payload,
|
||||||
'推进视觉小说失败',
|
'推进视觉小说失败',
|
||||||
options.signal,
|
options.signal,
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return readVisualNovelRuntimeRunFromSse(response, {
|
return readVisualNovelRuntimeRunFromSse(response, {
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import type {
|
|||||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||||
import { createCreationAgentClient } from '../creation-agent';
|
import { createCreationAgentClient } from '../creation-agent';
|
||||||
|
import {
|
||||||
|
buildRuntimeGuestAuthOptions,
|
||||||
|
buildRuntimeGuestHeaders,
|
||||||
|
type RuntimeGuestRequestOptions,
|
||||||
|
} from '../runtimeGuestAuth';
|
||||||
|
|
||||||
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
|
const WOODEN_FISH_API_BASE = '/api/creation/wooden-fish/sessions';
|
||||||
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
|
const WOODEN_FISH_WORKS_API_BASE = '/api/creation/wooden-fish/works';
|
||||||
@@ -27,6 +32,13 @@ const WOODEN_FISH_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
|||||||
baseDelayMs: 120,
|
baseDelayMs: 120,
|
||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
};
|
};
|
||||||
|
const WOODEN_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||||
|
maxRetries: 1,
|
||||||
|
baseDelayMs: 120,
|
||||||
|
maxDelayMs: 360,
|
||||||
|
retryUnsafeMethods: true,
|
||||||
|
};
|
||||||
|
type WoodenFishRuntimeRequestOptions = RuntimeGuestRequestOptions;
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
WoodenFishActionRequest,
|
WoodenFishActionRequest,
|
||||||
@@ -204,24 +216,35 @@ export async function publishWoodenFishWork(profileId: string) {
|
|||||||
return normalizeWoodenFishWorkMutationResponse(response);
|
return normalizeWoodenFishWorkMutationResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startWoodenFishRuntimeRun(profileId: string) {
|
export async function startWoodenFishRuntimeRun(
|
||||||
|
profileId: string,
|
||||||
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
return requestJson<WoodenFishRunResponse>(
|
return requestJson<WoodenFishRunResponse>(
|
||||||
`${WOODEN_FISH_RUNTIME_API_BASE}/runs`,
|
`${WOODEN_FISH_RUNTIME_API_BASE}/runs`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ profileId }),
|
body: JSON.stringify({ profileId }),
|
||||||
},
|
},
|
||||||
'启动敲木鱼运行态失败',
|
'启动敲木鱼运行态失败',
|
||||||
|
{
|
||||||
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
|
...requestOptions,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkpointWoodenFishRun(
|
export async function checkpointWoodenFishRun(
|
||||||
runId: string,
|
runId: string,
|
||||||
payload: Omit<WoodenFishCheckpointRunRequest, 'clientEventId'>,
|
payload: Omit<WoodenFishCheckpointRunRequest, 'clientEventId'>,
|
||||||
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
const requestPayload: WoodenFishCheckpointRunRequest = {
|
const requestPayload: WoodenFishCheckpointRunRequest = {
|
||||||
...payload,
|
...payload,
|
||||||
clientEventId: `checkpoint-${runId}-${Date.now()}`,
|
clientEventId: `checkpoint-${runId}-${Date.now()}`,
|
||||||
@@ -233,17 +256,24 @@ export async function checkpointWoodenFishRun(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestPayload),
|
body: JSON.stringify(requestPayload),
|
||||||
},
|
},
|
||||||
'保存敲木鱼进度失败',
|
'保存敲木鱼进度失败',
|
||||||
|
{
|
||||||
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
|
...requestOptions,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function finishWoodenFishRun(
|
export async function finishWoodenFishRun(
|
||||||
runId: string,
|
runId: string,
|
||||||
payload: Omit<WoodenFishFinishRunRequest, 'clientEventId'>,
|
payload: Omit<WoodenFishFinishRunRequest, 'clientEventId'>,
|
||||||
|
options: WoodenFishRuntimeRequestOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||||
const requestPayload: WoodenFishFinishRunRequest = {
|
const requestPayload: WoodenFishFinishRunRequest = {
|
||||||
...payload,
|
...payload,
|
||||||
clientEventId: `finish-${runId}-${Date.now()}`,
|
clientEventId: `finish-${runId}-${Date.now()}`,
|
||||||
@@ -255,10 +285,15 @@ export async function finishWoodenFishRun(
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
...buildRuntimeGuestHeaders(options),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestPayload),
|
body: JSON.stringify(requestPayload),
|
||||||
},
|
},
|
||||||
'结束敲木鱼运行失败',
|
'结束敲木鱼运行失败',
|
||||||
|
{
|
||||||
|
retry: WOODEN_FISH_RUNTIME_WRITE_RETRY,
|
||||||
|
...requestOptions,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user