合并 master 并保留外部生成 worker 模式
合入 master 的生产健康巡检、JumpHop 和 SpacetimeDB 更新 保留外部生成 worker、队列/内联模式与 lease guard 口径 合并 Server-Provision 工具复用、health patrol 和外部生成 worker systemd 配置 补齐 SpacetimeDB 生成绑定并通过本地检查
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -899,6 +899,23 @@ test('platform public work detail flow resolves like intent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow respects configured like disable', () => {
|
||||
expect(
|
||||
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle'), [
|
||||
{
|
||||
sourceType: 'puzzle',
|
||||
likeEnabled: false,
|
||||
remixEnabled: true,
|
||||
likeDisabledMessage: '拼图点赞维护中。',
|
||||
remixDisabledMessage: '拼图改造维护中。',
|
||||
},
|
||||
]),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: '拼图点赞维护中。',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves remix intent', () => {
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('big-fish')),
|
||||
@@ -969,13 +986,31 @@ test('platform public work detail flow resolves remix intent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow respects configured remix disable', () => {
|
||||
expect(
|
||||
resolvePlatformPublicWorkRemixIntent(buildRpgEntry(), [
|
||||
{
|
||||
sourceType: 'custom-world',
|
||||
likeEnabled: true,
|
||||
remixEnabled: false,
|
||||
likeDisabledMessage: 'RPG 点赞维护中。',
|
||||
remixDisabledMessage: 'RPG 改造维护中。',
|
||||
},
|
||||
]),
|
||||
).toEqual({
|
||||
type: 'unsupported',
|
||||
errorMessage: 'RPG 改造维护中。',
|
||||
});
|
||||
});
|
||||
|
||||
test('platform public work detail flow resolves edit intent for draft-backed works', () => {
|
||||
const bigFishEntry = buildTypedEntry('big-fish');
|
||||
expect(resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps()))
|
||||
.toEqual({
|
||||
type: 'edit-big-fish',
|
||||
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
||||
});
|
||||
expect(
|
||||
resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps()),
|
||||
).toEqual({
|
||||
type: 'edit-big-fish',
|
||||
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
||||
});
|
||||
|
||||
const selectedPuzzleDetail = buildPuzzleWork({
|
||||
profileId: 'puzzle-profile',
|
||||
@@ -1153,7 +1188,10 @@ test('platform public work detail flow resolves edit intent for unsupported and
|
||||
|
||||
const edutainmentEntry = buildTypedEntry('edutainment');
|
||||
expect(
|
||||
resolvePlatformPublicWorkEditIntent(edutainmentEntry, buildEditIntentDeps()),
|
||||
resolvePlatformPublicWorkEditIntent(
|
||||
edutainmentEntry,
|
||||
buildEditIntentDeps(),
|
||||
),
|
||||
).toEqual({
|
||||
type: 'resolve-edutainment-draft',
|
||||
entry: edutainmentEntry,
|
||||
|
||||
@@ -97,6 +97,14 @@ export type PlatformPublicWorkDetailOpenStrategy =
|
||||
|
||||
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
|
||||
|
||||
export type PlatformPublicWorkInteractionConfig = {
|
||||
sourceType: string;
|
||||
likeEnabled: boolean;
|
||||
remixEnabled: boolean;
|
||||
likeDisabledMessage: string;
|
||||
remixDisabledMessage: string;
|
||||
};
|
||||
|
||||
export type PlatformPublicWorkLikeIntent =
|
||||
| {
|
||||
type: 'like-big-fish';
|
||||
@@ -678,9 +686,55 @@ export function resolvePlatformPublicWorkActionMode(
|
||||
: 'remix';
|
||||
}
|
||||
|
||||
export function getPlatformPublicWorkInteractionSourceType(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
) {
|
||||
return 'sourceType' in entry ? entry.sourceType : 'custom-world';
|
||||
}
|
||||
|
||||
function resolveConfiguredPublicWorkInteractionBlock(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
configs: readonly PlatformPublicWorkInteractionConfig[] | null | undefined,
|
||||
action: 'like' | 'remix',
|
||||
): PlatformPublicWorkLikeIntent | PlatformPublicWorkRemixIntent | null {
|
||||
const sourceType = getPlatformPublicWorkInteractionSourceType(entry);
|
||||
const config = configs?.find((item) => item.sourceType === sourceType);
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action === 'like' && !config.likeEnabled) {
|
||||
return {
|
||||
type: 'unsupported',
|
||||
errorMessage:
|
||||
config.likeDisabledMessage.trim() || '该作品类型暂不支持点赞。',
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'remix' && !config.remixEnabled) {
|
||||
return {
|
||||
type: 'unsupported',
|
||||
errorMessage:
|
||||
config.remixDisabledMessage.trim() || '该作品类型暂不支持改造。',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolvePlatformPublicWorkLikeIntent(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
configs?: readonly PlatformPublicWorkInteractionConfig[] | null,
|
||||
): PlatformPublicWorkLikeIntent {
|
||||
const configuredBlock = resolveConfiguredPublicWorkInteractionBlock(
|
||||
entry,
|
||||
configs,
|
||||
'like',
|
||||
);
|
||||
if (configuredBlock) {
|
||||
return configuredBlock as PlatformPublicWorkLikeIntent;
|
||||
}
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return {
|
||||
type: 'like-big-fish',
|
||||
@@ -760,7 +814,17 @@ export function resolvePlatformPublicWorkLikeIntent(
|
||||
|
||||
export function resolvePlatformPublicWorkRemixIntent(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
configs?: readonly PlatformPublicWorkInteractionConfig[] | null,
|
||||
): PlatformPublicWorkRemixIntent {
|
||||
const configuredBlock = resolveConfiguredPublicWorkInteractionBlock(
|
||||
entry,
|
||||
configs,
|
||||
'remix',
|
||||
);
|
||||
if (configuredBlock) {
|
||||
return configuredBlock as PlatformPublicWorkRemixIntent;
|
||||
}
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return {
|
||||
type: 'remix-big-fish',
|
||||
@@ -933,8 +997,9 @@ export function resolvePlatformPublicWorkEditIntent(
|
||||
|
||||
if (isVisualNovelGalleryEntry(entry)) {
|
||||
const work =
|
||||
deps.visualNovelWorks?.find((item) => item.profileId === entry.profileId) ??
|
||||
null;
|
||||
deps.visualNovelWorks?.find(
|
||||
(item) => item.profileId === entry.profileId,
|
||||
) ?? null;
|
||||
if (!work) {
|
||||
return {
|
||||
type: 'blocked',
|
||||
|
||||
@@ -1695,6 +1695,28 @@ function buildMockJumpHopWork(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockJumpHopRuntimeRun(
|
||||
work: JumpHopWorkProfileResponse,
|
||||
overrides: Partial<JumpHopRuntimeRunSnapshotResponse> = {},
|
||||
): JumpHopRuntimeRunSnapshotResponse {
|
||||
return {
|
||||
runId: 'jump-hop-run-1',
|
||||
profileId: work.summary.profileId,
|
||||
ownerUserId: work.summary.ownerUserId,
|
||||
status: 'playing',
|
||||
currentPlatformIndex: 0,
|
||||
successfulJumpCount: 0,
|
||||
durationMs: 0,
|
||||
score: 0,
|
||||
combo: 0,
|
||||
path: work.path,
|
||||
lastJump: null,
|
||||
startedAtMs: 1_779_999_000_000,
|
||||
finishedAtMs: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockBarkBattleWork(
|
||||
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||
): BarkBattleWorkSummary {
|
||||
@@ -6991,6 +7013,89 @@ test('logged out public jump-hop detail starts runtime without requireAuth', asy
|
||||
expect(requireAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('direct jump hop runtime route without work code returns platform home', async () => {
|
||||
window.history.replaceState(null, '', '/runtime/jump-hop');
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.pathname).toBe('/');
|
||||
});
|
||||
expect(window.location.search).toBe('');
|
||||
expect(jumpHopClient.getGalleryDetail).not.toHaveBeenCalled();
|
||||
expect(jumpHopClient.startRun).not.toHaveBeenCalled();
|
||||
expect(await screen.findByRole('button', { name: '创作' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('direct jump hop runtime route with public work code starts published run', async () => {
|
||||
const publishedJumpHopWork = buildMockJumpHopWork({
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-direct-1',
|
||||
profileId: 'jump-hop-profile-direct-12345678',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'jump-hop-session-direct-1',
|
||||
themeText: '星星果园',
|
||||
workTitle: '星星果园跳一跳',
|
||||
workDescription: '沿着水果一路弹跳。',
|
||||
themeTags: ['果园', '星星'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
playCount: 8,
|
||||
updatedAt: '2026-06-07T10:00:00.000Z',
|
||||
publishedAt: '2026-06-07T10:00:00.000Z',
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
});
|
||||
const publishedJumpHopRun = buildMockJumpHopRuntimeRun(publishedJumpHopWork, {
|
||||
runId: 'jump-hop-run-direct-1',
|
||||
ownerUserId: '',
|
||||
});
|
||||
|
||||
window.history.replaceState(null, '', '/runtime/jump-hop?work=JH-12345678');
|
||||
vi.mocked(jumpHopClient.getGalleryDetail).mockResolvedValue({
|
||||
item: publishedJumpHopWork,
|
||||
});
|
||||
vi.mocked(jumpHopClient.startRun).mockResolvedValue({
|
||||
run: publishedJumpHopRun,
|
||||
});
|
||||
vi.mocked(jumpHopClient.getLeaderboard).mockResolvedValue({
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
items: [],
|
||||
viewerBest: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: () => {},
|
||||
requireAuth: vi.fn(),
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(jumpHopClient.getGalleryDetail).toHaveBeenCalledWith('JH-12345678');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(jumpHopClient.startRun).toHaveBeenCalledWith(
|
||||
publishedJumpHopWork.summary.profileId,
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(jumpHopClient.getWorkDetail).not.toHaveBeenCalled();
|
||||
expect(window.location.pathname).toBe('/runtime/jump-hop');
|
||||
expect(window.location.search).toContain('work=JH-12345678');
|
||||
});
|
||||
|
||||
test('owned public puzzle detail edits original draft instead of remixing', async () => {
|
||||
const user = userEvent.setup();
|
||||
const ownedPuzzleWork = {
|
||||
|
||||
@@ -65,7 +65,7 @@ test('jump hop workspace submits theme payload after required field is filled',
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
characterPrompt: '内置默认 3D 角色',
|
||||
tilePrompt: '云朵跳台主题的正面30度视角主题物体图集,物体本身作为跳跃落点',
|
||||
tilePrompt: '云朵跳台主题的3D立方体主题身份方块包装图集',
|
||||
endMoodPrompt: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ function buildJumpHopWorkspacePayload(
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
characterPrompt: '内置默认 3D 角色',
|
||||
tilePrompt: `${themeText}主题的正面30度视角主题物体图集,物体本身作为跳跃落点`,
|
||||
tilePrompt: `${themeText}主题的3D立方体主题身份方块包装图集`,
|
||||
endMoodPrompt: null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user