Merge remote-tracking branch 'origin/master' into codex/wooden-fish-template

This commit is contained in:
2026-05-24 20:35:32 +08:00
12 changed files with 207 additions and 67 deletions

View File

@@ -74,6 +74,14 @@
- 验证方式:目标机 `nginx -T 2>/dev/null | grep client_max_body_size` 应看到 `client_max_body_size 64m;`;大于 1 MiB 的参考图请求不再在 Nginx 层直接 413access log 应出现有效 `upstream_status`
- 关联文档:`deploy/nginx/README.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-24 跳一跳推荐页允许未登录直达运行态并记录匿名游玩埋点
- 背景:推荐页的跳一跳作品在未登录时曾被前端登录门禁拦住,导致公开推荐流无法直接游玩;同时游玩埋点如果只接受登录态 userId会让匿名启动和匿名重开被静默丢失。
- 决策:跳一跳推荐页的运行态启动、跳跃和重开路由统一使用可选鉴权;未登录时仍允许进入运行态,并把 `work_play_start` 以匿名语义记录下来,而不是伪造用户身份或直接跳过埋点。
- 影响范围:`api-server` 跳一跳 runtime 路由、`work_play_tracking`、推荐页进入运行态逻辑、匿名推荐试玩测试、平台入口 / 玩法链路文档。
- 验证:登录态和未登录态都能从推荐页进入运行态;`work_play_start` 事件在匿名时仍产生metadata 带匿名标记。
- 关联:`server-rs/crates/api-server/src/jump_hop.rs``server-rs/crates/api-server/src/auth.rs``server-rs/crates/api-server/src/work_play_tracking.rs``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`
## 2026-05-22 抓大鹅素材生成改为关卡整图派生三图
- 背景:旧抓大鹅素材链路按物品 5x5 sheet、纯背景和独立容器图分开生产难以保证背景、UI、容器和物品风格一致也让结果页继续暴露背景 / 容器重生成入口。
@@ -83,6 +91,7 @@
- 验证方式:执行 `cargo test -p api-server match3d --manifest-path server-rs\Cargo.toml``npm run test -- src/components/match3d-result/Match3DResultView.test.tsx src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/services/match3dSpritesheetParser.test.ts src/services/match3dGeneratedModelCache.test.ts``npm run typecheck``npm run check:encoding`
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-18 Rust 手写模块入口统一不用 mod.rs
- 背景Rust 目录模块同时存在 `mod.rs` 与同名 `.rs` 两种入口形式,前次拆分已让 `spacetime-client/src/mapper.rs` 采用同名入口;继续新增 `mod.rs` 会让文件定位和评审口径不一致。

View File

@@ -169,6 +169,14 @@
- 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
- 关联:`server-rs/crates/api-server/src/tracking_outbox.rs``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 跳一跳推荐页匿名直玩要同步放行 runtime 路由和埋点
- 现象:推荐页能看到跳一跳公开卡片,但未登录点击后会被登录门禁拦住,或者进入运行态后没有 `work_play_start` 记录。
- 原因:前端只改了展示层登录门禁,后端 runtime 路由仍要求 bearer auth或 tracking helper 仍把匿名请求当成无效输入直接丢弃。
- 处理:`/api/runtime/jump-hop/runs``/jump``/restart` 改为可选鉴权;未登录时直接允许启动、跳跃和重开,同时让 `work_play_tracking` 接受 `Option` 用户身份并在 metadata 中标记匿名语义,不要伪造 userId。
- 验证:未登录推荐页可以直接进入跳一跳运行态,且 `work_play_start` 事件仍会落库或出现在 outbox 中metadata 含匿名标记。
- 关联:`server-rs/crates/api-server/src/jump_hop.rs``server-rs/crates/api-server/src/auth.rs``server-rs/crates/api-server/src/work_play_tracking.rs``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`
## release tracking outbox 权限错误先查 env 缺失
- 现象release 机器 `journalctl -u genarrative-api.service` 每秒刷 `tracking outbox 定时封存 active 文件失败 error=Permission denied (os error 13)``tracking outbox 批量写入 SpacetimeDB 失败`
@@ -177,6 +185,7 @@
- 验证:`tr '\0' '\n' < /proc/$(systemctl show genarrative-api.service -p MainPID --value)/environ | grep GENARRATIVE_TRACKING_OUTBOX_DIR` 应指向 `/var/lib/genarrative/tracking-outbox`;重启后当前 PID 不再出现 `Permission denied (os error 13)`
- 关联:`scripts/deploy/production-api-deploy.sh``scripts/jenkins-server-provision.sh``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 外部 API 失败没法追溯先查 external_api_call_failure
- 现象VectorEngine 图片生成 / 编辑接口对前端只表现为 `502` / `504` 或“上游服务请求失败”,但难以区分是请求发送失败、上游 429/5xx、响应解析失败、未返回图片还是下载图片失败。

View File

@@ -148,6 +148,13 @@ Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`
- `npm run check:server-rs-ddd`
- `npm run dev:api-server` 后请求 `/healthz`
其中推荐页匿名游玩与 `work_play_start` 相关改动,至少要补跑:
```bash
cargo check -p api-server --manifest-path server-rs/Cargo.toml
npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "logged out recommend page can enter runtime without login gate|logged out desktop recommend page renders runtime directly without login gate"
```
涉及 SpacetimeDB schema 时必须补:
```bash

View File

@@ -120,6 +120,8 @@ RPG / 拼图等运行态存档选择入口统一在个人中心 `常用功能 >
平台首页推荐、精选、最新、公开详情、搜索、已玩作品和公开试玩统一按 `sourceType='jump-hop'``JH-*` 公开作品号识别跳一跳作品;从公开详情或推荐流启动运行态时,若卡片摘要不足以携带角色图、地块图集和路径配置,必须先补读完整 work profile 再传入运行态。平台壳层必须同步注册 `jump-hop-workspace``jump-hop-generating``jump-hop-result``jump-hop-runtime``jump-hop-gallery-detail` 阶段,并在 `appPageRoutes.ts` 映射 `/creation/jump-hop/workspace``/creation/jump-hop/generating``/creation/jump-hop/result``/gallery/jump-hop/detail``/runtime/jump-hop`,同时持有 session、work、run、gallery、busy/error 与生成进度状态,避免只合入渲染分支但遗漏状态源或分享路径导致 typecheck 失败、刷新回首页。
推荐页允许未登录直接游玩跳一跳运行态;`/api/runtime/jump-hop/runs``/jump``/restart` 采用可选鉴权,未登录时仍记录 `work_play_start`,但埋点需标记匿名语义。
## 敲木鱼
对外名称:`敲木鱼`。工程域:`wooden-fish`。PRD 见 `docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`

View File

@@ -59,17 +59,44 @@ pub async fn require_bearer_auth(
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let Some(authenticated) = authenticate_request(&state, &request)? else {
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
};
request.extensions_mut().insert(authenticated.clone());
let mut response = next.run(request).await;
response.extensions_mut().insert(authenticated);
Ok(response)
}
pub async fn attach_optional_bearer_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
if let Some(authenticated) = authenticate_request(&state, &request)? {
request.extensions_mut().insert(authenticated.clone());
let mut response = next.run(request).await;
response.extensions_mut().insert(authenticated);
return Ok(response);
}
Ok(next.run(request).await)
}
fn authenticate_request(
state: &AppState,
request: &Request,
) -> Result<Option<AuthenticatedAccessToken>, AppError> {
if allows_internal_forwarded_auth(request.uri().path())
&& let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers())
{
request
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims.clone()));
let mut response = next.run(request).await;
response
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims));
return Ok(response);
return Ok(Some(AuthenticatedAccessToken::new(claims)));
}
if !request.headers().contains_key(AUTHORIZATION) {
return Ok(None);
}
let bearer_token = extract_bearer_token(request.headers())?;
@@ -145,16 +172,7 @@ pub async fn require_bearer_auth(
.with_message("当前登录态已失效,请重新登录"));
}
request
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims.clone()));
let mut response = next.run(request).await;
response
.extensions_mut()
.insert(AuthenticatedAccessToken::new(claims));
Ok(response)
Ok(Some(AuthenticatedAccessToken::new(claims)))
}
pub async fn inspect_auth_claims(

View File

@@ -17,8 +17,12 @@ use spacetime_client::SpacetimeClientError;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
api_response::json_success_body,
auth::AuthenticatedAccessToken,
http_error::AppError,
request_context::RequestContext,
state::AppState,
work_play_tracking::{record_work_play_start_after_success, WorkPlayTrackingDraft},
};
const JUMP_HOP_PROVIDER: &str = "jump-hop";
@@ -26,6 +30,8 @@ const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation";
const JUMP_HOP_RUNTIME_PROVIDER: &str = "jump-hop-runtime";
const JUMP_HOP_TEMPLATE_ID: &str = "jump-hop";
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";
pub async fn create_jump_hop_session(
State(state): State<AppState>,
@@ -170,14 +176,18 @@ pub async fn get_jump_hop_runtime_work(
pub async fn start_jump_hop_run(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopStartRunRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
ensure_non_empty(&request_context, &payload.profile_id, "profileId")?;
let authenticated = maybe_authenticated.as_ref().map(|Extension(authenticated)| authenticated);
let owner_user_id = authenticated
.map(|authenticated| authenticated.claims().user_id().to_string())
.unwrap_or_else(|| JUMP_HOP_ANONYMOUS_RUNTIME_OWNER_ID.to_string());
let run = state
.spacetime_client()
.start_jump_hop_run(payload, authenticated.claims().user_id().to_string())
.start_jump_hop_run(payload, owner_user_id.clone())
.await
.map_err(|error| {
jump_hop_error_response(
@@ -187,6 +197,24 @@ pub async fn start_jump_hop_run(
)
})?;
record_work_play_start_after_success(
&state,
&request_context,
build_jump_hop_work_play_tracking_draft(
authenticated,
run.profile_id.clone(),
JUMP_HOP_RUNTIME_RUNS_ROUTE,
)
.owner_user_id(run.owner_user_id.clone())
.run_id(run.run_id.clone())
.profile_id(run.profile_id.clone())
.extra(json!({
"runStatus": run.status,
"isAnonymous": maybe_authenticated.is_none(),
})),
)
.await;
Ok(json_success_body(
Some(&request_context),
JumpHopRunResponse { run },
@@ -197,18 +225,18 @@ pub async fn jump_hop_run_jump(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopJumpRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
let owner_user_id = maybe_authenticated
.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
.spacetime_client()
.jump_hop_run_jump(
run_id,
authenticated.claims().user_id().to_string(),
payload,
)
.jump_hop_run_jump(run_id, owner_user_id, payload)
.await
.map_err(|error| {
jump_hop_error_response(
@@ -228,18 +256,18 @@ pub async fn restart_jump_hop_run(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
maybe_authenticated: Option<Extension<AuthenticatedAccessToken>>,
payload: Result<Json<JumpHopRestartRunRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let Json(payload) = jump_hop_json(payload, &request_context, JUMP_HOP_RUNTIME_PROVIDER)?;
let owner_user_id = maybe_authenticated
.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
.spacetime_client()
.restart_jump_hop_run(
run_id,
authenticated.claims().user_id().to_string(),
payload,
)
.restart_jump_hop_run(run_id, owner_user_id, payload)
.await
.map_err(|error| {
jump_hop_error_response(
@@ -298,6 +326,19 @@ pub async fn get_jump_hop_gallery_detail(
))
}
fn build_jump_hop_work_play_tracking_draft(
authenticated: Option<&AuthenticatedAccessToken>,
work_id: impl Into<String>,
source_route: &'static str,
) -> WorkPlayTrackingDraft {
match authenticated {
Some(authenticated) => {
WorkPlayTrackingDraft::new("jump-hop", work_id, authenticated, source_route)
}
None => WorkPlayTrackingDraft::anonymous("jump-hop", work_id, source_route),
}
}
fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraftResponse {
JumpHopDraftResponse {
template_id: JUMP_HOP_TEMPLATE_ID.to_string(),

View File

@@ -4,7 +4,7 @@ use axum::{
};
use crate::{
auth::require_bearer_auth,
auth::{attach_optional_bearer_auth, require_bearer_auth},
jump_hop::{
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,
@@ -51,21 +51,21 @@ pub fn router(state: AppState) -> Router<AppState> {
"/api/runtime/jump-hop/runs",
post(start_jump_hop_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route(
"/api/runtime/jump-hop/runs/{run_id}/jump",
post(jump_hop_run_jump).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route(
"/api/runtime/jump-hop/runs/{run_id}/restart",
post(restart_jump_hop_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
attach_optional_bearer_auth,
)),
)
.route("/api/runtime/jump-hop/gallery", get(list_jump_hop_gallery))

View File

@@ -13,7 +13,7 @@ pub(crate) const WORK_PLAY_START_EVENT_KEY: &str = "work_play_start";
pub(crate) struct WorkPlayTrackingDraft {
pub play_type: &'static str,
pub work_id: String,
pub user_id: String,
pub user_id: Option<String>,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub run_id: Option<String>,
@@ -28,7 +28,28 @@ impl WorkPlayTrackingDraft {
authenticated: &AuthenticatedAccessToken,
source_route: &'static str,
) -> Self {
let user_id = authenticated.claims().user_id().to_string();
Self::with_user_id(
play_type,
work_id,
Some(authenticated.claims().user_id().to_string()),
source_route,
)
}
pub(crate) fn anonymous(
play_type: &'static str,
work_id: impl Into<String>,
source_route: &'static str,
) -> Self {
Self::with_user_id(play_type, work_id, None, source_route)
}
fn with_user_id(
play_type: &'static str,
work_id: impl Into<String>,
user_id: Option<String>,
source_route: &'static str,
) -> Self {
Self {
play_type,
work_id: work_id.into(),
@@ -91,7 +112,11 @@ async fn record_work_play_start_input_after_success(
"workId": draft.work_id,
"sourceRoute": draft.source_route,
});
metadata["userId"] = json!(draft.user_id);
if let Some(user_id) = draft.user_id.as_deref() {
metadata["userId"] = json!(user_id);
} else {
metadata["userKind"] = json!("anonymous");
}
if let Some(owner_user_id) = draft.owner_user_id.as_deref() {
metadata["ownerUserId"] = json!(owner_user_id);
}
@@ -108,7 +133,7 @@ async fn record_work_play_start_input_after_success(
let mut tracking = TrackingEventDraft::new(WORK_PLAY_START_EVENT_KEY, draft.play_type);
tracking.scope_kind = RuntimeTrackingScopeKind::Work;
tracking.scope_id = draft.work_id;
tracking.user_id = Some(draft.user_id);
tracking.user_id = draft.user_id;
tracking.owner_user_id = draft.owner_user_id;
tracking.profile_id = draft.profile_id;
tracking.metadata = metadata;

View File

@@ -7492,7 +7492,10 @@ export function PlatformEntryFlowShellImpl({
try {
const [detail, runResponse] = await Promise.all([
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
jumpHopClient.startRun(normalizedProfileId),
jumpHopClient.startRun(
normalizedProfileId,
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
),
]);
if (detail?.item) {
setJumpHopWork(detail.item);
@@ -11666,8 +11669,6 @@ export function PlatformEntryFlowShellImpl({
if (
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!platformBootstrap.isAuthenticated ||
!platformBootstrap.canReadProtectedData ||
platformBootstrap.isLoadingPlatform
) {
return;
@@ -11719,9 +11720,7 @@ export function PlatformEntryFlowShellImpl({
jumpHopRun,
isStartingRecommendEntry,
match3dRun,
platformBootstrap.canReadProtectedData,
platformBootstrap.isLoadingPlatform,
platformBootstrap.isAuthenticated,
platformBootstrap.platformTab,
puzzleRun,
recommendRuntimeEntries,

View File

@@ -2495,17 +2495,34 @@ test('logged out recommend cover opens login modal again', async () => {
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged out desktop recommend page renders cover only', () => {
test('logged out desktop recommend page renders runtime directly', () => {
mockDesktopLayout();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
});
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
expect(document.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(screen.queryByText('今日游戏')).toBeNull();
expect(screen.queryByText('作品分类')).toBeNull();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
});
test('logged out recommend page can enter runtime without login gate', () => {
mockDesktopLayout();
const openLoginModal = vi.fn();
const onOpenGalleryDetail = vi.fn();
renderLoggedOutHomeView(openLoginModal, {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onOpenGalleryDetail,
recommendRuntimeContent: <div data-testid="recommend-runtime"></div>,
});
expect(screen.queryByRole('button', { name: / /u })).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(openLoginModal).not.toHaveBeenCalled();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged in recommend page uses gated recommend detail callback', async () => {
@@ -2581,7 +2598,7 @@ test('logged in recommend page uses gated recommend detail callback', async () =
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged out mobile recommend page renders cover instead of runtime', () => {
test('logged out mobile recommend page renders runtime instead of cover', () => {
const onOpenGalleryDetail = vi.fn();
renderLoggedOutHomeView(
vi.fn(),
@@ -2593,13 +2610,13 @@ test('logged out mobile recommend page renders cover instead of runtime', () =>
'home',
);
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(document.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(
document.querySelector('.platform-public-work-card__cover'),
).toBeNull();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: / /u }));
expect(screen.queryByRole('button', { name: / /u })).toBeNull();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -2611,7 +2628,8 @@ test('mobile recommend loading state is themed instead of hardcoded black', () =
recommendRuntimeContent: null,
});
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
expect(document.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(screen.getByText('加载中...')).toBeTruthy();
});
test('logged in recommend runtime preloads adjacent work previews and drag switches like video feed', () => {

View File

@@ -5273,16 +5273,6 @@ export function RpgEntryHomeView({
...
</div>
</section>
) : !isAuthenticated && activeRecommendEntry ? (
<section className="platform-recommend-runtime-panel">
<RecommendCoverOnlyCard
entry={activeRecommendEntry}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
activeRecommendEntry,
)}
onClick={openActiveRecommendEntry}
/>
</section>
) : recommendRuntimeError ? (
<section className="platform-recommend-runtime-panel">
<button

View File

@@ -15,7 +15,11 @@ import type {
JumpHopWorkspaceCreateRequest,
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import { type ApiRetryOptions, requestJson } from '../apiClient';
import {
type ApiRequestOptions,
type ApiRetryOptions,
requestJson,
} from '../apiClient';
import { createCreationAgentClient } from '../creation-agent';
const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions';
@@ -26,6 +30,14 @@ const JUMP_HOP_RUNTIME_READ_RETRY: ApiRetryOptions = {
baseDelayMs: 120,
maxDelayMs: 360,
};
type JumpHopRuntimeRequestOptions = Pick<
ApiRequestOptions,
| 'authImpact'
| 'skipAuth'
| 'skipRefresh'
| 'notifyAuthStateChange'
| 'clearAuthOnUnauthorized'
>;
export type {
JumpHopActionRequest,
@@ -208,7 +220,10 @@ export async function publishJumpHopWork(profileId: string) {
return normalizeJumpHopWorkMutationResponse(response);
}
export async function startJumpHopRuntimeRun(profileId: string) {
export async function startJumpHopRuntimeRun(
profileId: string,
options: JumpHopRuntimeRequestOptions = {},
) {
return requestJson<JumpHopRunResponse>(
`${JUMP_HOP_RUNTIME_API_BASE}/runs`,
{
@@ -219,6 +234,13 @@ export async function startJumpHopRuntimeRun(profileId: string) {
body: JSON.stringify({ profileId }),
},
'启动跳一跳运行态失败',
{
authImpact: options.authImpact,
skipAuth: options.skipAuth,
skipRefresh: options.skipRefresh,
notifyAuthStateChange: options.notifyAuthStateChange,
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
},
);
}