Merge branch 'master' into editor-agent-right-click
# Conflicts: # src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx
This commit is contained in:
@@ -114,10 +114,6 @@ WECHAT_MOCK_DISPLAY_NAME="微信旅人"
|
||||
WECHAT_MOCK_AVATAR_URL=""
|
||||
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=""
|
||||
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
|
||||
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED="true"
|
||||
WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID="m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU"
|
||||
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE="formal"
|
||||
|
||||
# Model name for chat completions.
|
||||
VITE_LLM_MODEL="gpt-5.4-mini"
|
||||
GENARRATIVE_LLM_PROVIDER="openai-compatible"
|
||||
@@ -125,9 +121,6 @@ GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
|
||||
GENARRATIVE_LLM_API_KEY=""
|
||||
GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
|
||||
|
||||
# Optional: enable upstream web search for RPG story text generation.
|
||||
RPG_LLM_WEB_SEARCH_ENABLED="true"
|
||||
|
||||
# Server-side DashScope endpoint and API key used by the local scene-image proxy.
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
+287
-8
@@ -58,6 +58,70 @@ module.exports = {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/active-main.tsx',
|
||||
'src/ActiveApp.tsx',
|
||||
'src/AuthenticatedApp.tsx',
|
||||
'src/hooks/useGameSettings.ts',
|
||||
'src/routing/activeApp*.ts',
|
||||
'src/routing/activeApp*.tsx',
|
||||
'src/components/auth/**/*.{ts,tsx}',
|
||||
'src/components/common/**/*.{ts,tsx}',
|
||||
'src/components/creation-home/**/*.{ts,tsx}',
|
||||
'src/components/image-editor/**/*.{ts,tsx}',
|
||||
'src/components/project/**/*.{ts,tsx}',
|
||||
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileModalShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRechargeModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx',
|
||||
'src/components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx',
|
||||
'src/components/platform-entry/platformActiveProfileModel.ts',
|
||||
'src/components/platform-entry/platformEntryActiveTypes.ts',
|
||||
'src/components/platform-entry/platformProfileFundsModel.ts',
|
||||
'src/components/platform-entry/platformProfileHostClipboard.ts',
|
||||
'src/components/platform-entry/usePlatformProfileCenterController.ts',
|
||||
'src/services/image-editor/**/*.{ts,tsx}',
|
||||
'src/services/platform-entry/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
'**/components/rpg-entry/**',
|
||||
'**/components/*-creation/**',
|
||||
'**/components/*-result/**',
|
||||
'**/components/*-runtime/**',
|
||||
'**/components/creation-agent/**',
|
||||
'**/components/creative-agent/**',
|
||||
'**/components/custom-world-*/**',
|
||||
'**/components/unified-creation/**',
|
||||
'**/services/rpg-entry/**',
|
||||
'**/services/rpg-runtime/**',
|
||||
'**/services/*-creation/**',
|
||||
'**/services/*-runtime/**',
|
||||
'**/services/*-works/**',
|
||||
'**/services/creation-agent/**',
|
||||
'**/services/creative-agent/**',
|
||||
'**/services/puzzle-*/**',
|
||||
'**/services/storyEngine/**',
|
||||
],
|
||||
message: '现役前端不得重新导入已退役的创作模板模块。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/game-canvas/**/*.tsx'],
|
||||
rules: {
|
||||
@@ -71,12 +135,6 @@ module.exports = {
|
||||
'simple-import-sort/exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/components/match3d-runtime/Match3DPhysicsBoard.tsx'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
@@ -96,22 +154,240 @@ module.exports = {
|
||||
'dist_check',
|
||||
'dist_check_monster_position',
|
||||
'coverage',
|
||||
'build',
|
||||
'output',
|
||||
'retired/**',
|
||||
'node_modules',
|
||||
'server-rs/target',
|
||||
'server-rs/target-*',
|
||||
'apps/desktop-shell/src-tauri/target',
|
||||
'target',
|
||||
'src/main.tsx',
|
||||
'src/App.tsx',
|
||||
'src/routing/appPageRoutes.ts',
|
||||
'src/routing/appPageRoutes.test.ts',
|
||||
'src/routing/appRoutes.tsx',
|
||||
'src/routing/appRoutes.test.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/appTitle.test.ts',
|
||||
'src/components/CharacterAnimator.tsx',
|
||||
'src/components/*.ts',
|
||||
'src/components/*.tsx',
|
||||
'!src/components/ResolvedAssetAudio.tsx',
|
||||
'!src/components/ResolvedAssetImage.tsx',
|
||||
'!src/components/ResolvedAssetVideo.tsx',
|
||||
'src/hooks/*.ts',
|
||||
'src/hooks/*.tsx',
|
||||
'!src/hooks/useGameSettings.ts',
|
||||
'!src/hooks/useHostNavigationCanGoBack.ts',
|
||||
'!src/hooks/useHostNavigationCanGoBack.test.tsx',
|
||||
'!src/hooks/useResolvedAssetReadUrl.ts',
|
||||
'!src/hooks/useResolvedAssetReadUrl.test.tsx',
|
||||
'src/persistence/*.ts',
|
||||
'src/persistence/*.tsx',
|
||||
'!src/persistence/gameSettingsStorage.ts',
|
||||
'!src/persistence/gameSettingsStorage.test.ts',
|
||||
'!src/persistence/storage.ts',
|
||||
'!src/persistence/storage.test.ts',
|
||||
'src/routing/*.ts',
|
||||
'src/routing/*.tsx',
|
||||
'!src/routing/activeAppPageRoutes.ts',
|
||||
'!src/routing/activeAppPageRoutes.test.ts',
|
||||
'!src/routing/activeAppRoutes.tsx',
|
||||
'!src/routing/activeAppRoutes.test.ts',
|
||||
'src/services/*.ts',
|
||||
'src/services/*.tsx',
|
||||
'!src/services/activeAppTitle.ts',
|
||||
'!src/services/activeAppTitle.test.ts',
|
||||
'!src/services/apiClient.ts',
|
||||
'!src/services/apiClient.test.ts',
|
||||
'!src/services/assetReadUrlService.ts',
|
||||
'!src/services/assetReadUrlService.test.ts',
|
||||
'!src/services/authService.ts',
|
||||
'!src/services/authService.test.ts',
|
||||
'!src/services/clipboard.ts',
|
||||
'!src/services/clipboard.test.ts',
|
||||
'!src/services/frontendRuntimeConfigService.ts',
|
||||
'!src/services/frontendRuntimeConfigService.test.ts',
|
||||
'!src/services/sseStream.ts',
|
||||
'!src/services/sseStream.test.ts',
|
||||
'src/AdventurePanel.tsx',
|
||||
'src/AdventureEntityModal.tsx',
|
||||
'src/App.tsx',
|
||||
'src/App.test.tsx',
|
||||
'src/CharacterCreation.tsx',
|
||||
'src/RpgRuntimeApp.tsx',
|
||||
'src/routing/appPageRoutes.ts',
|
||||
'src/routing/appPageRoutes.test.ts',
|
||||
'src/routing/appRoutes.tsx',
|
||||
'src/routing/appRoutes.test.ts',
|
||||
'src/*PlaygroundApp.tsx',
|
||||
'src/ChildMotionDemoApp.tsx',
|
||||
'src/Match3DPlaygroundApp.tsx',
|
||||
'src/components/child-motion-demo/**',
|
||||
'src/components/asset-studio/**',
|
||||
'src/components/bark-battle-creation/**',
|
||||
'src/components/big-fish-creation/**',
|
||||
'src/components/big-fish-result/**',
|
||||
'src/components/big-fish-runtime/**',
|
||||
'src/components/creation-agent/**',
|
||||
'src/components/creative-agent/**',
|
||||
'src/components/custom-world-agent/**',
|
||||
'src/components/custom-world-home/**',
|
||||
'src/components/edutainment-creation/**',
|
||||
'src/components/edutainment-result/**',
|
||||
'src/components/edutainment-runtime/**',
|
||||
'src/components/game-canvas/**',
|
||||
'src/components/jump-hop-result/**',
|
||||
'src/components/jump-hop-runtime/**',
|
||||
'src/components/match3d-result/**',
|
||||
'src/components/match3d-runtime/**',
|
||||
'src/components/puzzle-clear-creation/**',
|
||||
'src/components/puzzle-clear-result/**',
|
||||
'src/components/puzzle-clear-runtime/**',
|
||||
'src/components/puzzle-gallery/**',
|
||||
'src/components/puzzle-result/**',
|
||||
'src/components/puzzle-runtime/**',
|
||||
'src/components/rpg-creation-asset-studio/**',
|
||||
'src/components/rpg-creation-result/**',
|
||||
'src/components/rpg-runtime-panels/**',
|
||||
'src/components/square-hole-creation/**',
|
||||
'src/components/square-hole-result/**',
|
||||
'src/components/square-hole-runtime/**',
|
||||
'src/components/unified-creation/**',
|
||||
'src/components/visual-novel-creation/**',
|
||||
'src/components/visual-novel-result/**',
|
||||
'src/components/visual-novel-runtime/**',
|
||||
'src/components/wooden-fish-result/**',
|
||||
'src/components/wooden-fish-runtime/**',
|
||||
'src/components/common/PublishShare*',
|
||||
'src/components/common/publishShare*',
|
||||
'src/components/platform-entry/PlatformEntryCreation*',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
|
||||
'src/components/platform-entry/platformEntryTypes.ts',
|
||||
'src/components/platform-entry/PlatformEntryHome*',
|
||||
'src/components/platform-entry/PlatformEntryWorld*',
|
||||
'src/components/platform-entry/PlatformMobileHome*',
|
||||
'src/components/platform-entry/PlatformWork*',
|
||||
'src/components/platform-entry/PlatformDraft*',
|
||||
'src/components/platform-entry/PlatformTask*',
|
||||
'src/components/platform-entry/PlatformError*',
|
||||
'src/components/platform-entry/barkBattle*',
|
||||
'src/components/platform-entry/platformCreation*',
|
||||
'src/components/platform-entry/platformDialog*',
|
||||
'src/components/platform-entry/platformDraft*',
|
||||
'src/components/platform-entry/platformEdutainment*',
|
||||
'src/components/platform-entry/platformEntryCreation*',
|
||||
'src/components/platform-entry/platformExternal*',
|
||||
'src/components/platform-entry/platformGeneration*',
|
||||
'src/components/platform-entry/platformHost*',
|
||||
'src/components/platform-entry/platformMiniGame*',
|
||||
'src/components/platform-entry/platformPlayed*',
|
||||
'src/components/platform-entry/platformPublic*',
|
||||
'src/components/platform-entry/platformPuzzle*',
|
||||
'src/components/platform-entry/platformRecommend*',
|
||||
'src/components/platform-entry/platformRpg*',
|
||||
'src/components/platform-entry/platformSelection*',
|
||||
'src/components/platform-entry/puzzleDraft*',
|
||||
'src/components/platform-entry/usePlatformCreation*',
|
||||
'src/components/platform-entry/usePlatformEntry*',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl/**',
|
||||
'src/components/platform-entry/platformMatch3DRuntimeProfile*',
|
||||
'src/components/unified-creation/workspaces/JumpHopCreationWorkspace*',
|
||||
'src/components/unified-creation/workspaces/Match3DCreationWorkspace*',
|
||||
'src/components/rpg-creation-editor/**',
|
||||
'src/components/rpg-entry/**',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.test.tsx',
|
||||
'src/components/custom-world-home/CustomWorldCreationHub.testAdapter.tsx',
|
||||
'src/components/custom-world-home/creationWorkShelf.test.ts',
|
||||
'src/components/rpg-runtime-shell/**',
|
||||
'src/hooks/rpg-runtime-story/**',
|
||||
'src/hooks/rpg-session/**',
|
||||
'src/hooks/combat/**',
|
||||
'src/hooks/useCombatFlow.ts',
|
||||
'src/hooks/useStoryOptions.ts',
|
||||
'src/prompts/customWorldPrompts.ts',
|
||||
'src/services/ai.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/appTitle.test.ts',
|
||||
'src/services/miniGameDraftGenerationProgress.ts',
|
||||
'src/services/creationEntryConfigService.ts',
|
||||
'src/services/creationUrlState*',
|
||||
'src/services/customWorld*',
|
||||
'src/services/input-devices/**',
|
||||
'src/services/publicWorkCode.ts',
|
||||
'src/services/runtimeGuestAuth.ts',
|
||||
'src/services/runtimeRequest*',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
'src/services/useMocapInput*',
|
||||
'src/services/wechatMiniProgramSubscribe*',
|
||||
'src/services/bark-battle-creation/**',
|
||||
'src/services/bark-battle-runtime/**',
|
||||
'src/services/big-fish-creation/**',
|
||||
'src/services/big-fish-gallery/**',
|
||||
'src/services/big-fish-runtime/**',
|
||||
'src/services/big-fish-works/**',
|
||||
'src/services/creation-agent/**',
|
||||
'src/services/creation-audio/**',
|
||||
'src/services/creative-agent/**',
|
||||
'src/services/edutainment-baby-drawing/**',
|
||||
'src/services/edutainment-baby-object/**',
|
||||
'src/services/child-motion-demo/**',
|
||||
'src/services/jump-hop/**',
|
||||
'src/services/match3d-creation/**',
|
||||
'src/services/match3d-runtime/**',
|
||||
'src/services/match3d-works/**',
|
||||
'src/services/match3dGeneratedModelCache*',
|
||||
'src/services/match3dSpritesheetParser*',
|
||||
'src/services/puzzle-agent/**',
|
||||
'src/services/puzzle-gallery/**',
|
||||
'src/services/puzzle-onboarding/**',
|
||||
'src/services/puzzle-runtime/**',
|
||||
'src/services/puzzle-works/**',
|
||||
'src/services/rpg-creation/**',
|
||||
'src/services/rpg-entry/**',
|
||||
'src/services/rpg-runtime/**',
|
||||
'src/services/square-hole-creation/**',
|
||||
'src/services/square-hole-runtime/**',
|
||||
'src/services/square-hole-works/**',
|
||||
'src/services/storyEngine/**',
|
||||
'src/services/visual-novel-creation/**',
|
||||
'src/services/visual-novel-runtime/**',
|
||||
'src/services/visual-novel-works/**',
|
||||
'src/services/wooden-fish/**',
|
||||
'src/types.ts',
|
||||
'src/types/**',
|
||||
'src/uiAssets.ts',
|
||||
'scripts/loadtest/**',
|
||||
'packages/shared/src/contracts/jumpHop.ts',
|
||||
'packages/shared/src/contracts/match3dAgent.ts',
|
||||
'packages/shared/src/contracts/match3dRuntime.ts',
|
||||
'packages/shared/src/contracts/match3dWorks.ts',
|
||||
'packages/shared/src/contracts/barkBattle*',
|
||||
'packages/shared/src/contracts/bigFish*',
|
||||
'packages/shared/src/contracts/creationAgent*',
|
||||
'packages/shared/src/contracts/creationAudio*',
|
||||
'packages/shared/src/contracts/creativeAgent*',
|
||||
'packages/shared/src/contracts/customWorld*',
|
||||
'packages/shared/src/contracts/edutainment*',
|
||||
'packages/shared/src/contracts/playTypes*',
|
||||
'packages/shared/src/contracts/publicWork*',
|
||||
'packages/shared/src/contracts/puzzle*',
|
||||
'packages/shared/src/contracts/rpg*',
|
||||
'packages/shared/src/contracts/squareHole*',
|
||||
'packages/shared/src/contracts/story*',
|
||||
'packages/shared/src/contracts/visualNovel*',
|
||||
'packages/shared/src/contracts/woodenFish*',
|
||||
'scripts/export-match3d-resource-pipeline*',
|
||||
'scripts/generate-child-motion-demo-assets.mjs',
|
||||
'src/services/puzzle-clear/**',
|
||||
'src/games/**',
|
||||
'src/data/**',
|
||||
'src/prompts/**',
|
||||
'apps/admin-web/src/pages/AdminCreationEntrySwitchPage*',
|
||||
'apps/admin-web/src/pages/AdminGrayReleaseConfigPage*',
|
||||
'apps/admin-web/src/pages/AdminWorkVisibilityPage*',
|
||||
'src/services/recommendedRuntimeGuestLaunch.test.ts',
|
||||
'src/data/sceneEncounterPreviews.ts',
|
||||
'public/Icons',
|
||||
@@ -130,7 +406,10 @@ module.exports = {
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-refresh/only-export-components': ['error', {allowConstantExport: true}],
|
||||
'react-refresh/only-export-components': [
|
||||
'error',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
@@ -143,7 +422,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'no-constant-condition': 'error',
|
||||
'no-console': ['error', {allow: ['warn', 'error']}],
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-useless-escape': 'error',
|
||||
'prefer-const': 'error',
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
AdminCreateAccountResponse,
|
||||
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
||||
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
|
||||
AdminCreationEntryConfigResponse,
|
||||
AdminDashboardQuery,
|
||||
AdminDashboardResponse,
|
||||
AdminDatabaseTableListResponse,
|
||||
@@ -23,7 +22,6 @@ import type {
|
||||
AdminEditorShowcaseListQuery,
|
||||
AdminEditorShowcaseListResponse,
|
||||
AdminEditorShowcaseReviewRequest,
|
||||
AdminFeatureGateConfigResponse,
|
||||
AdminLoginResponse,
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
@@ -40,24 +38,17 @@ import type {
|
||||
AdminTrackingEventListResponse,
|
||||
AdminUpdateAccountRequest,
|
||||
AdminUpdateAccountResponse,
|
||||
AdminUpdateWorkVisibilityRequest,
|
||||
AdminUpdateWorkVisibilityResponse,
|
||||
AdminUploadedEditorShowcaseCampaignImage,
|
||||
AdminUpsertCreationEntryEventBannersRequest,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
AdminUpsertEditorShowcaseCampaignRequest,
|
||||
AdminUpsertFeatureGateConfigRequest,
|
||||
AdminUpsertProfileInviteCodeRequest,
|
||||
AdminUpsertProfileRechargeProductRequest,
|
||||
AdminUpsertProfileRedeemCodeRequest,
|
||||
AdminUpsertProfileTaskConfigRequest,
|
||||
AdminUpsertProfileWalletConfigRequest,
|
||||
AdminUpsertPublicWorkInteractionConfigRequest,
|
||||
AdminUserDetailQuery,
|
||||
AdminUserDetailResponse,
|
||||
AdminWalletRestrictionRequest,
|
||||
AdminWalletRestrictionResponse,
|
||||
AdminWorkVisibilityListResponse,
|
||||
ApiErrorEnvelope,
|
||||
ApiMeta,
|
||||
ApiSuccessEnvelope,
|
||||
@@ -194,7 +185,7 @@ export function getAdminMe(token: string) {
|
||||
}
|
||||
|
||||
export function listAdminAccounts(token: string) {
|
||||
return request<AdminAccountListResponse>('/admin/api/accounts', {token});
|
||||
return request<AdminAccountListResponse>('/admin/api/accounts', { token });
|
||||
}
|
||||
|
||||
export function createAdminAccount(
|
||||
@@ -215,7 +206,7 @@ export function updateAdminAccount(
|
||||
) {
|
||||
return request<AdminUpdateAccountResponse>(
|
||||
`/admin/api/accounts/${encodeURIComponent(accountId)}`,
|
||||
{method: 'PUT', token, body: payload},
|
||||
{ method: 'PUT', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,74 +266,6 @@ export function listAdminTrackingEventKeys(token: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminFeatureGateConfig(token: string) {
|
||||
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export function upsertAdminFeatureGateConfig(
|
||||
token: string,
|
||||
payload: AdminUpsertFeatureGateConfigRequest,
|
||||
) {
|
||||
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
|
||||
method: 'PUT',
|
||||
token,
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminCreationEntryConfig(token: string) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config',
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function upsertAdminCreationEntryConfig(
|
||||
token: string,
|
||||
payload: AdminUpsertCreationEntryTypeConfigRequest,
|
||||
) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存创作入口公告表单序列化后的后端传输字段。 */
|
||||
export function upsertAdminCreationEntryBanners(
|
||||
token: string,
|
||||
payload: AdminUpsertCreationEntryEventBannersRequest,
|
||||
) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config/banners',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存公开作品详情页点赞 / 改造能力配置。 */
|
||||
export function upsertAdminPublicWorkInteractions(
|
||||
token: string,
|
||||
payload: AdminUpsertPublicWorkInteractionConfigRequest,
|
||||
) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config/interactions',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminEditorGenerationPricing(token: string) {
|
||||
return request<EditorGenerationPricingConfigPayload>(
|
||||
'/admin/api/editor-generation-pricing',
|
||||
@@ -364,27 +287,6 @@ export function upsertAdminEditorGenerationPricing(
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminWorkVisibility(token: string) {
|
||||
return request<AdminWorkVisibilityListResponse>(
|
||||
'/admin/api/works/visibility',
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAdminWorkVisibility(
|
||||
token: string,
|
||||
payload: AdminUpdateWorkVisibilityRequest,
|
||||
) {
|
||||
return request<AdminUpdateWorkVisibilityResponse>(
|
||||
'/admin/api/works/visibility',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminAssetReadUrl(
|
||||
token: string,
|
||||
query: AdminAssetReadUrlQuery,
|
||||
@@ -471,18 +373,19 @@ export async function uploadAdminEditorShowcaseCampaignImage(
|
||||
): Promise<AdminUploadedEditorShowcaseCampaignImage> {
|
||||
const contentType = resolveAdminImageContentType(file);
|
||||
const dimensions = await readAdminImageFileDimensions(file);
|
||||
const response = await request<AdminCreateEditorShowcaseCampaignImageUploadTicketResponse>(
|
||||
'/admin/api/editor-showcase/campaign/image-upload-ticket',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: {
|
||||
fileName: file.name.trim() || 'showcase-campaign.png',
|
||||
contentType,
|
||||
contentLength: file.size,
|
||||
} satisfies AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
||||
},
|
||||
);
|
||||
const response =
|
||||
await request<AdminCreateEditorShowcaseCampaignImageUploadTicketResponse>(
|
||||
'/admin/api/editor-showcase/campaign/image-upload-ticket',
|
||||
{
|
||||
method: 'POST',
|
||||
token,
|
||||
body: {
|
||||
fileName: file.name.trim() || 'showcase-campaign.png',
|
||||
contentType,
|
||||
contentLength: file.size,
|
||||
} satisfies AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
||||
},
|
||||
);
|
||||
await postAdminDirectUploadFile(response.upload, file);
|
||||
const objectKey = response.upload.objectKey.trim().replace(/^\/+/u, '');
|
||||
return {
|
||||
@@ -630,17 +533,14 @@ export function listAdminRechargeOrders(
|
||||
) {
|
||||
return request<AdminRechargeOrderListResponse>(
|
||||
`/admin/api/profile/recharge-orders${buildAdminRechargeOrderListQuery(query)}`,
|
||||
{token},
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminUserDetail(
|
||||
token: string,
|
||||
query: AdminUserDetailQuery,
|
||||
) {
|
||||
export function getAdminUserDetail(token: string, query: AdminUserDetailQuery) {
|
||||
return request<AdminUserDetailResponse>(
|
||||
`/admin/api/profile/users/detail${buildAdminUserDetailQuery(query)}`,
|
||||
{token},
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -650,7 +550,7 @@ export function previewAdminRechargeRefund(
|
||||
) {
|
||||
return request<AdminRechargeRefundPreviewResponse>(
|
||||
'/admin/api/profile/recharge-refunds/preview',
|
||||
{method: 'POST', token, body: payload},
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -660,7 +560,7 @@ export function executeAdminRechargeRefund(
|
||||
) {
|
||||
return request<AdminRechargeRefundActionResponse>(
|
||||
'/admin/api/profile/recharge-refunds/execute',
|
||||
{method: 'POST', token, body: payload},
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -670,7 +570,7 @@ export function registerAdminRechargeRefund(
|
||||
) {
|
||||
return request<AdminRechargeRefundActionResponse>(
|
||||
'/admin/api/profile/recharge-refunds/register',
|
||||
{method: 'POST', token, body: payload},
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -680,7 +580,7 @@ export function resolveAdminRechargeRefundManualReview(
|
||||
) {
|
||||
return request<AdminRechargeRefundActionResponse>(
|
||||
'/admin/api/profile/recharge-refunds/manual-review/resolve',
|
||||
{method: 'POST', token, body: payload},
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -690,7 +590,7 @@ export function updateAdminWalletRestriction(
|
||||
) {
|
||||
return request<AdminWalletRestrictionResponse>(
|
||||
'/admin/api/profile/wallet-restriction',
|
||||
{method: 'POST', token, body: payload},
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -710,10 +610,7 @@ function buildAssetReadUrlQuery(query: AdminAssetReadUrlQuery) {
|
||||
if (objectKey) {
|
||||
params.set('objectKey', objectKey);
|
||||
} else if (legacyPublicPath) {
|
||||
params.set(
|
||||
'legacyPublicPath',
|
||||
`/${legacyPublicPath.replace(/^\/+/u, '')}`,
|
||||
);
|
||||
params.set('legacyPublicPath', `/${legacyPublicPath.replace(/^\/+/u, '')}`);
|
||||
}
|
||||
if (
|
||||
typeof query.expireSeconds === 'number' &&
|
||||
@@ -731,7 +628,10 @@ function resolveAdminImageContentType(file: File) {
|
||||
if (declaredType.startsWith('image/')) {
|
||||
return declaredType;
|
||||
}
|
||||
const extension = file.name.trim().toLowerCase().match(/\.([a-z0-9]+)$/u)?.[1];
|
||||
const extension = file.name
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.match(/\.([a-z0-9]+)$/u)?.[1];
|
||||
if (extension === 'jpg' || extension === 'jpeg') {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
@@ -286,98 +286,6 @@ export interface AdminTrackingEventListQuery {
|
||||
exportAll?: boolean;
|
||||
}
|
||||
|
||||
export interface AdminFeatureGateConfigPayload {
|
||||
gateKey: string;
|
||||
enabled: boolean;
|
||||
rolloutPercent: number;
|
||||
allowUserIds: string[];
|
||||
allowUserTags: string[];
|
||||
denyUserIds: string[];
|
||||
description: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AdminFeatureGateConfigResponse {
|
||||
gates: AdminFeatureGateConfigPayload[];
|
||||
}
|
||||
|
||||
export type AdminUpsertFeatureGateConfigRequest = Omit<
|
||||
AdminFeatureGateConfigPayload,
|
||||
'updatedAt'
|
||||
>;
|
||||
|
||||
/** 后台创作入口配置响应,同时包含模板入口和独立公告配置。 */
|
||||
export interface AdminCreationEntryConfigResponse {
|
||||
entries: AdminCreationEntryTypeConfigPayload[];
|
||||
eventBanners: AdminCreationEntryEventBannerPayload[];
|
||||
publicWorkInteractions: PublicWorkInteractionConfigPayload[];
|
||||
}
|
||||
|
||||
/** 后台创作入口公告位配置项;旧结构化 banner 字段仅保留兼容。 */
|
||||
export interface AdminCreationEntryEventBannerPayload {
|
||||
title: string;
|
||||
description: string;
|
||||
coverImageSrc: string;
|
||||
prizePoolMudPoints: number;
|
||||
startsAtText: string;
|
||||
endsAtText: string;
|
||||
renderMode: 'structured' | 'html';
|
||||
htmlCode?: string | null;
|
||||
}
|
||||
|
||||
/** 后台单个创作模板入口配置,公告不再绑定在某一个入口上。 */
|
||||
export interface AdminCreationEntryTypeConfigPayload {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
imageSrc: string;
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
sortOrder: number;
|
||||
categoryId: string;
|
||||
categoryLabel: string;
|
||||
categorySortOrder: number;
|
||||
updatedAtMicros: number;
|
||||
unifiedCreationSpec?: UnifiedCreationSpecPayload | null;
|
||||
}
|
||||
|
||||
/** 后台保存创作模板入口开关与统一创作契约的请求体。 */
|
||||
export interface AdminUpsertCreationEntryTypeConfigRequest {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
imageSrc: string;
|
||||
visible: boolean;
|
||||
open: boolean;
|
||||
sortOrder: number;
|
||||
categoryId: string;
|
||||
categoryLabel: string;
|
||||
categorySortOrder: number;
|
||||
unifiedCreationSpec?: UnifiedCreationSpecPayload | null;
|
||||
}
|
||||
|
||||
/** 后台保存创作入口公告表单序列化结果的请求体。 */
|
||||
export interface AdminUpsertCreationEntryEventBannersRequest {
|
||||
/** 传输字段沿用后端契约,内容由后台表单生成。 */
|
||||
eventBannersJson: string;
|
||||
}
|
||||
|
||||
/** 后台公开作品详情页互动能力配置项。 */
|
||||
export interface PublicWorkInteractionConfigPayload {
|
||||
sourceType: string;
|
||||
likeEnabled: boolean;
|
||||
remixEnabled: boolean;
|
||||
likeDisabledMessage: string;
|
||||
remixDisabledMessage: string;
|
||||
}
|
||||
|
||||
/** 后台保存公开作品点赞 / 改造能力配置请求体。 */
|
||||
export interface AdminUpsertPublicWorkInteractionConfigRequest {
|
||||
publicWorkInteractions: PublicWorkInteractionConfigPayload[];
|
||||
}
|
||||
|
||||
/** 图片画布生成模型泥点定价配置。 */
|
||||
export type EditorGenerationPricingUnitPayload = 'perGeneration' | 'perSecond';
|
||||
|
||||
@@ -391,55 +299,6 @@ export interface EditorGenerationPricingConfigPayload {
|
||||
models: Record<string, EditorGenerationModelPricingPayload>;
|
||||
}
|
||||
|
||||
/** 后台统一创作工作台契约表单的传输结构。 */
|
||||
export interface UnifiedCreationSpecPayload {
|
||||
playId: string;
|
||||
title: string;
|
||||
mudPointCost: number;
|
||||
workspaceStage: string;
|
||||
generationStage: string;
|
||||
resultStage: string;
|
||||
fields: UnifiedCreationFieldPayload[];
|
||||
}
|
||||
|
||||
/** 后台统一创作字段契约,保存前会校验字段类型和必填标记。 */
|
||||
export interface UnifiedCreationFieldPayload {
|
||||
id: string;
|
||||
kind: 'text' | 'select' | 'image' | 'audio';
|
||||
label: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface AdminWorkVisibilityEntryPayload {
|
||||
sourceType: string;
|
||||
workId: string;
|
||||
profileId: string;
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
coverImageSrc?: string | null;
|
||||
visible: boolean;
|
||||
publishedAtMicros?: number | null;
|
||||
updatedAtMicros: number;
|
||||
}
|
||||
|
||||
export interface AdminWorkVisibilityListResponse {
|
||||
entries: AdminWorkVisibilityEntryPayload[];
|
||||
}
|
||||
|
||||
export interface AdminUpdateWorkVisibilityRequest {
|
||||
sourceType: string;
|
||||
profileId: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface AdminUpdateWorkVisibilityResponse {
|
||||
entry: AdminWorkVisibilityEntryPayload;
|
||||
}
|
||||
|
||||
export interface AdminEditorAssetListQuery {
|
||||
cursor?: string | null;
|
||||
ownerUserId?: string | null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
formatAdminApiError,
|
||||
@@ -17,33 +17,30 @@ import {
|
||||
getStoredAdminToken,
|
||||
setStoredAdminToken,
|
||||
} from '../auth/adminAuthStore';
|
||||
import {AdminAccountsPage} from '../pages/AdminAccountsPage';
|
||||
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
|
||||
import {AdminDashboardPage} from '../pages/AdminDashboardPage';
|
||||
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
|
||||
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
|
||||
import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage';
|
||||
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
|
||||
import {AdminEditorShowcaseReviewPage} from '../pages/AdminEditorShowcaseReviewPage';
|
||||
import {AdminGrayReleaseConfigPage} from '../pages/AdminGrayReleaseConfigPage';
|
||||
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
||||
import {AdminLoginPage} from '../pages/AdminLoginPage';
|
||||
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
|
||||
import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage';
|
||||
import {AdminRechargeOrderPage} from '../pages/AdminRechargeOrderPage';
|
||||
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
|
||||
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
|
||||
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
|
||||
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
|
||||
import {AdminWorkVisibilityPage} from '../pages/AdminWorkVisibilityPage';
|
||||
import type {AdminRouteId} from './adminRoutes';
|
||||
import { AdminAccountsPage } from '../pages/AdminAccountsPage';
|
||||
import { AdminDashboardPage } from '../pages/AdminDashboardPage';
|
||||
import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage';
|
||||
import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
|
||||
import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
|
||||
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
|
||||
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
|
||||
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
|
||||
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
||||
import { AdminOverviewPage } from '../pages/AdminOverviewPage';
|
||||
import { AdminProfileWalletConfigPage } from '../pages/AdminProfileWalletConfigPage';
|
||||
import { AdminRechargeOrderPage } from '../pages/AdminRechargeOrderPage';
|
||||
import { AdminRechargeProductPage } from '../pages/AdminRechargeProductPage';
|
||||
import { AdminRedeemCodePage } from '../pages/AdminRedeemCodePage';
|
||||
import { AdminTaskConfigPage } from '../pages/AdminTaskConfigPage';
|
||||
import { AdminTrackingEventsPage } from '../pages/AdminTrackingEventsPage';
|
||||
import type { AdminRouteId } from './adminRoutes';
|
||||
import {
|
||||
getAccessibleAdminRoutes,
|
||||
resolveAccessibleAdminRoute,
|
||||
resolveAdminRoute,
|
||||
routeHash,
|
||||
} from './adminRoutes';
|
||||
import {AdminShell} from './AdminShell';
|
||||
import { AdminShell } from './AdminShell';
|
||||
|
||||
type SessionStatus = 'checking' | 'guest' | 'authenticated';
|
||||
|
||||
@@ -157,17 +154,20 @@ export function AdminApp() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLogin = useCallback(async (username: string, password: string) => {
|
||||
const response = await loginAdmin(username, password);
|
||||
setStoredAdminToken(response.token);
|
||||
setToken(response.token);
|
||||
setAdmin(response.admin);
|
||||
setTaskConfigResult(null);
|
||||
setProfileWalletConfigResult(null);
|
||||
setRechargeProductResult(null);
|
||||
setLoginNotice('');
|
||||
setStatus('authenticated');
|
||||
}, []);
|
||||
const handleLogin = useCallback(
|
||||
async (username: string, password: string) => {
|
||||
const response = await loginAdmin(username, password);
|
||||
setStoredAdminToken(response.token);
|
||||
setToken(response.token);
|
||||
setAdmin(response.admin);
|
||||
setTaskConfigResult(null);
|
||||
setProfileWalletConfigResult(null);
|
||||
setRechargeProductResult(null);
|
||||
setLoginNotice('');
|
||||
setStatus('authenticated');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleUnauthorized = useCallback(
|
||||
(message = '登录状态已失效') => {
|
||||
@@ -227,12 +227,6 @@ export function AdminApp() {
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'gray-release' ? (
|
||||
<AdminGrayReleaseConfigPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'redeem' ? (
|
||||
<AdminRedeemCodePage
|
||||
token={token}
|
||||
@@ -245,25 +239,6 @@ export function AdminApp() {
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'creation-announcement' ? (
|
||||
<AdminCreationEntrySwitchPage
|
||||
mode="announcements"
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'creation-entry' ? (
|
||||
<AdminCreationEntrySwitchPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'work-visibility' ? (
|
||||
<AdminWorkVisibilityPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'tasks' ? (
|
||||
<AdminTaskConfigPage
|
||||
result={taskConfigResult}
|
||||
@@ -313,10 +288,7 @@ export function AdminApp() {
|
||||
/>
|
||||
) : null}
|
||||
{activeRouteId === 'accounts' ? (
|
||||
<AdminAccountsPage
|
||||
token={token}
|
||||
onUnauthorized={handleUnauthorized}
|
||||
/>
|
||||
<AdminAccountsPage token={token} onUnauthorized={handleUnauthorized} />
|
||||
) : null}
|
||||
</AdminShell>
|
||||
);
|
||||
|
||||
@@ -4,16 +4,12 @@ import {
|
||||
Bug,
|
||||
Coins,
|
||||
Database,
|
||||
Eye,
|
||||
GitBranch,
|
||||
Images,
|
||||
LayoutDashboard,
|
||||
ListChecks,
|
||||
LogOut,
|
||||
Megaphone,
|
||||
ReceiptText,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
Star,
|
||||
Table2,
|
||||
TicketCheck,
|
||||
@@ -21,10 +17,10 @@ import {
|
||||
Users,
|
||||
WalletCards,
|
||||
} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type {AdminSessionPayload} from '../api/adminApiTypes';
|
||||
import type {AdminRouteDefinition, AdminRouteId} from './adminRoutes';
|
||||
import type { AdminSessionPayload } from '../api/adminApiTypes';
|
||||
import type { AdminRouteDefinition, AdminRouteId } from './adminRoutes';
|
||||
|
||||
interface AdminShellProps {
|
||||
admin: AdminSessionPayload;
|
||||
@@ -41,7 +37,6 @@ const routeIcons = {
|
||||
tables: Database,
|
||||
debug: Bug,
|
||||
tracking: Table2,
|
||||
'gray-release': GitBranch,
|
||||
redeem: TicketPercent,
|
||||
invite: TicketCheck,
|
||||
'profile-wallet': WalletCards,
|
||||
@@ -51,9 +46,6 @@ const routeIcons = {
|
||||
'editor-generation-pricing': Coins,
|
||||
'editor-showcase': Star,
|
||||
'editor-assets': Images,
|
||||
'creation-announcement': Megaphone,
|
||||
'creation-entry': SlidersHorizontal,
|
||||
'work-visibility': Eye,
|
||||
accounts: Users,
|
||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {expect, test} from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
adminRoutes,
|
||||
@@ -19,19 +19,6 @@ test('后台默认进入 Dashboard', () => {
|
||||
expect(routeHash('dashboard')).toBe('#dashboard');
|
||||
});
|
||||
|
||||
// 中文注释:后台入口公告必须作为独立导航存在,避免公告表单被误藏在入口开关页。
|
||||
test('后台入口公告路由可通过导航和 hash 访问', () => {
|
||||
expect(adminRoutes).toContainEqual({
|
||||
id: 'creation-announcement',
|
||||
label: '入口公告',
|
||||
hash: '#creation-announcement',
|
||||
});
|
||||
expect(resolveAdminRoute('#creation-announcement')).toBe(
|
||||
'creation-announcement',
|
||||
);
|
||||
expect(routeHash('creation-announcement')).toBe('#creation-announcement');
|
||||
});
|
||||
|
||||
test('后台模型定价路由可通过导航和 hash 访问', () => {
|
||||
expect(adminRoutes).toContainEqual({
|
||||
id: 'editor-generation-pricing',
|
||||
@@ -46,16 +33,12 @@ test('后台模型定价路由可通过导航和 hash 访问', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('后台灰度发布路由可通过导航和 hash 访问', () => {
|
||||
expect(adminRoutes).toContainEqual({
|
||||
id: 'gray-release',
|
||||
label: '灰度发布',
|
||||
hash: '#gray-release',
|
||||
});
|
||||
expect(resolveAdminRoute('#gray-release')).toBe('gray-release');
|
||||
expect(routeHash('gray-release')).toBe('#gray-release');
|
||||
test('后台不再暴露旧创作模板管理路由', () => {
|
||||
expect(resolveAdminRoute('#creation-entry')).toBe('dashboard');
|
||||
expect(resolveAdminRoute('#creation-announcement')).toBe('dashboard');
|
||||
expect(resolveAdminRoute('#gray-release')).toBe('dashboard');
|
||||
expect(resolveAdminRoute('#work-visibility')).toBe('dashboard');
|
||||
});
|
||||
|
||||
test('后台素材查询路由可通过导航和 hash 访问', () => {
|
||||
expect(adminRoutes).toContainEqual({
|
||||
id: 'editor-assets',
|
||||
@@ -92,7 +75,7 @@ test('owner 可访问全部业务 Tab 和账号管理', () => {
|
||||
tabPermissions: [],
|
||||
});
|
||||
expect(routes).toEqual(adminRoutes);
|
||||
expect(routes.at(-1)).toMatchObject({id: 'accounts', ownerOnly: true});
|
||||
expect(routes.at(-1)).toMatchObject({ id: 'accounts', ownerOnly: true });
|
||||
});
|
||||
|
||||
test('member 只访问已分配 Tab 且无权 hash 回落到第一项', () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ export type AdminRouteId =
|
||||
| 'tables'
|
||||
| 'debug'
|
||||
| 'tracking'
|
||||
| 'gray-release'
|
||||
| 'redeem'
|
||||
| 'invite'
|
||||
| 'profile-wallet'
|
||||
@@ -15,9 +14,6 @@ export type AdminRouteId =
|
||||
| 'editor-generation-pricing'
|
||||
| 'editor-showcase'
|
||||
| 'editor-assets'
|
||||
| 'creation-announcement'
|
||||
| 'creation-entry'
|
||||
| 'work-visibility'
|
||||
| 'accounts';
|
||||
|
||||
export type AdminTabPermission = Exclude<AdminRouteId, 'accounts'>;
|
||||
@@ -31,25 +27,25 @@ export interface AdminRouteDefinition {
|
||||
}
|
||||
|
||||
export const adminRoutes: AdminRouteDefinition[] = [
|
||||
{id: 'dashboard', label: 'Dashboard', hash: '#dashboard'},
|
||||
{id: 'overview', label: '服务总览', hash: '#overview'},
|
||||
{id: 'tables', label: '表查询', hash: '#tables'},
|
||||
{id: 'debug', label: 'API 调试', hash: '#debug'},
|
||||
{id: 'tracking', label: '埋点数据', hash: '#tracking'},
|
||||
{id: 'gray-release', label: '灰度发布', hash: '#gray-release'},
|
||||
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
||||
{id: 'invite', label: '邀请码', hash: '#invite'},
|
||||
{id: 'profile-wallet', label: '账号配置', hash: '#profile-wallet'},
|
||||
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
||||
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
|
||||
{id: 'recharge-orders', label: '充值管理', hash: '#recharge-orders'},
|
||||
{id: 'editor-generation-pricing', label: '模型定价', hash: '#editor-generation-pricing'},
|
||||
{id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase'},
|
||||
{id: 'editor-assets', label: '素材查询', hash: '#editor-assets'},
|
||||
{id: 'creation-announcement', label: '入口公告', hash: '#creation-announcement'},
|
||||
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
||||
{id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'},
|
||||
{id: 'accounts', label: '账号管理', hash: '#accounts', ownerOnly: true},
|
||||
{ id: 'dashboard', label: 'Dashboard', hash: '#dashboard' },
|
||||
{ id: 'overview', label: '服务总览', hash: '#overview' },
|
||||
{ id: 'tables', label: '表查询', hash: '#tables' },
|
||||
{ id: 'debug', label: 'API 调试', hash: '#debug' },
|
||||
{ id: 'tracking', label: '埋点数据', hash: '#tracking' },
|
||||
{ id: 'redeem', label: '兑换码', hash: '#redeem' },
|
||||
{ id: 'invite', label: '邀请码', hash: '#invite' },
|
||||
{ id: 'profile-wallet', label: '账号配置', hash: '#profile-wallet' },
|
||||
{ id: 'tasks', label: '任务配置', hash: '#tasks' },
|
||||
{ id: 'recharge-products', label: '充值商品', hash: '#recharge-products' },
|
||||
{ id: 'recharge-orders', label: '充值管理', hash: '#recharge-orders' },
|
||||
{
|
||||
id: 'editor-generation-pricing',
|
||||
label: '模型定价',
|
||||
hash: '#editor-generation-pricing',
|
||||
},
|
||||
{ id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' },
|
||||
{ id: 'editor-assets', label: '素材查询', hash: '#editor-assets' },
|
||||
{ id: 'accounts', label: '账号管理', hash: '#accounts', ownerOnly: true },
|
||||
];
|
||||
|
||||
export interface AdminRouteAccess {
|
||||
|
||||
@@ -21,8 +21,8 @@ describe('admin tracking event definitions', () => {
|
||||
expect(keys).toContain('auth_login_options_view');
|
||||
expect(keys).toContain('task_center_view');
|
||||
expect(keys).toContain('asset_upload_ticket_create');
|
||||
expect(keys).toContain('creative_agent_route_success');
|
||||
expect(keys).toContain('work_play_start');
|
||||
expect(keys).not.toContain('creative_agent_route_success');
|
||||
expect(keys).not.toContain('work_play_start');
|
||||
});
|
||||
|
||||
test('任务配置候选只开放适合个人任务的事件', () => {
|
||||
@@ -49,13 +49,15 @@ describe('admin tracking event definitions', () => {
|
||||
]);
|
||||
|
||||
expect(options.find(({ key }) => key === 'work_play_start')?.title).toBe(
|
||||
'作品开始游玩',
|
||||
'work_play_start',
|
||||
);
|
||||
expect(options.find(({ key }) => key === 'unknown_event')?.title).toBe(
|
||||
'未知事件',
|
||||
);
|
||||
expect(
|
||||
filterAdminTrackingEventKeyOptions(options, '作品').map(({ key }) => key),
|
||||
filterAdminTrackingEventKeyOptions(options, 'work_play').map(
|
||||
({ key }) => key,
|
||||
),
|
||||
).toEqual(['work_play_start']);
|
||||
});
|
||||
|
||||
@@ -65,7 +67,7 @@ describe('admin tracking event definitions', () => {
|
||||
).toEqual(['asset_upload_ticket_create']);
|
||||
expect(
|
||||
filterAdminTrackingEventDefinitions('work_play').map(({ key }) => key),
|
||||
).toEqual(['work_play_start']);
|
||||
).toEqual([]);
|
||||
expect(findAdminTrackingEventDefinition(' daily_login ')?.title).toBe(
|
||||
'每日登录',
|
||||
);
|
||||
|
||||
@@ -148,42 +148,6 @@ export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
scopeKind: 'user',
|
||||
remark: '领取个人任务奖励成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'save_archive_list_view',
|
||||
title: '存档列表查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人存档列表成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'save_archive_detail_view',
|
||||
title: '存档详情查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人存档详情成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_view',
|
||||
title: '浏览历史查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_record',
|
||||
title: '浏览历史写入',
|
||||
scopeKind: 'user',
|
||||
remark: '记录浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'browse_history_clear',
|
||||
title: '浏览历史清空',
|
||||
scopeKind: 'user',
|
||||
remark: '清空浏览历史成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'play_stats_view',
|
||||
title: '游玩统计查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取个人游玩统计成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'profile_analytics_metric_view',
|
||||
title: '个人指标查看',
|
||||
@@ -352,62 +316,6 @@ export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
scopeKind: 'user',
|
||||
remark: '更新运行设置成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_view',
|
||||
title: '运行快照查看',
|
||||
scopeKind: 'user',
|
||||
remark: '读取运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_save',
|
||||
title: '运行快照保存',
|
||||
scopeKind: 'user',
|
||||
remark: '保存运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'runtime_snapshot_delete',
|
||||
title: '运行快照删除',
|
||||
scopeKind: 'user',
|
||||
remark: '删除运行快照成功后记录。',
|
||||
},
|
||||
{
|
||||
key: 'puzzle_route_success',
|
||||
title: '拼图路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '拼图运行或创作接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'match3d_route_success',
|
||||
title: '抓大鹅路由成功',
|
||||
scopeKind: 'user',
|
||||
remark:
|
||||
'抓大鹅创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'square_hole_route_success',
|
||||
title: '方洞路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '方洞创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'custom_world_route_success',
|
||||
title: '自定义世界路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '自定义世界运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'creative_agent_route_success',
|
||||
title: '创意 Agent 路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '创意 Agent 接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'work_play_start',
|
||||
title: '作品开始游玩',
|
||||
scopeKind: 'work',
|
||||
remark:
|
||||
'拼图、抓大鹅、方洞、自定义世界、大鱼吃小鱼、Visual Novel 正式开始游玩时记录。',
|
||||
},
|
||||
];
|
||||
|
||||
export const adminProfileTaskTrackingEventDefinitions =
|
||||
|
||||
@@ -15,5 +15,12 @@
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
"include": ["src", "vite.config.ts"],
|
||||
"exclude": [
|
||||
"src/pages/AdminCreationEntrySwitchPage.tsx",
|
||||
"src/pages/AdminCreationEntrySwitchPage.test.tsx",
|
||||
"src/pages/AdminGrayReleaseConfigPage.tsx",
|
||||
"src/pages/AdminGrayReleaseConfigPage.test.tsx",
|
||||
"src/pages/AdminWorkVisibilityPage.tsx"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,11 +10,10 @@ Docker Compose
|
||||
├─ nginx :80 -> api-server:8082,负责静态站点、/admin/、/api/ 反代、upstream timing log、连接限制
|
||||
├─ api-server :8082,Linux release 构建,连接 compose 内 SpacetimeDB
|
||||
├─ external-generation-worker,独立 worker 进程,消费 external_generation_job 队列
|
||||
├─ otelcol :4317/4318,debug exporter,接收 traces / metrics / logs
|
||||
└─ k6 profile=loadtest 时临时启动,在 compose 网络内压 nginx
|
||||
└─ otelcol :4317/4318,debug exporter,接收 traces / metrics / logs
|
||||
```
|
||||
|
||||
当前容器模拟参数按 `genarrative-release` 服务器采样值收口为 2 vCPU / 2 GiB RAM / 4096 soft nofile / 768 worker_connections,并已在 compose 里落实到 `spacetimedb cpus=1.0 mem_limit=896m`、`api-server cpus=2.0 mem_limit=1g`、`external-generation-worker cpus=2.0 mem_limit=1g`、`nginx cpus=0.5 mem_limit=128m`、`otelcol cpus=0.25 mem_limit=128m`、`k6 cpus=1.0 mem_limit=512m`。SpacetimeDB 同时设置 `--page_pool_max_size=402653184`,给 reducer、订阅与运行时保留更多非 page pool 内存。
|
||||
当前容器模拟参数按 `genarrative-release` 服务器采样值收口为 2 vCPU / 2 GiB RAM / 4096 soft nofile / 768 worker_connections,并已在 compose 里落实到 `spacetimedb cpus=1.0 mem_limit=896m`、`api-server cpus=2.0 mem_limit=1g`、`external-generation-worker cpus=2.0 mem_limit=1g`、`nginx cpus=0.5 mem_limit=128m`、`otelcol cpus=0.25 mem_limit=128m`。SpacetimeDB 同时设置 `--page_pool_max_size=402653184`,给 reducer、订阅与运行时保留更多非 page pool 内存。
|
||||
容器 `api-server` 默认 `GENARRATIVE_API_WORKER_THREADS=4`,用于让 Tokio 在 2 vCPU 配额内有更多 I/O 调度 worker;该值不会突破 compose 里的 `cpus=2.0` CPU 上限。
|
||||
容器默认 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue`,用于验证 `api-server -> external_generation_job -> external-generation-worker` 链路;如只想本地同步排查 provider/OSS/SpacetimeDB 写回,可在本机 env 临时改为 `inline`,但该模式不会覆盖 worker 动态扩缩容验证。
|
||||
Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`。
|
||||
@@ -68,7 +67,8 @@ npm run container:up -- spacetimedb
|
||||
spacetime publish genarrative-loadtest --server http://127.0.0.1:13101 --module-path server-rs/crates/spacetime-module --yes --build-options="--debug"
|
||||
npm run container:up
|
||||
npm run container:ps
|
||||
curl -sS http://127.0.0.1:18080/api/runtime/puzzle/gallery
|
||||
curl -sS -H 'Authorization: Bearer <access-token>' \
|
||||
'http://127.0.0.1:18080/api/assets/history?kind=character_visual'
|
||||
```
|
||||
|
||||
查看日志:
|
||||
@@ -167,42 +167,9 @@ npm run container:down
|
||||
npm run container:down -- -v
|
||||
```
|
||||
|
||||
## 压测
|
||||
## 历史压测
|
||||
|
||||
k6 在 compose 网络内访问 `http://nginx`,避免 Windows 本机直连连接模型干扰 Linux 容器结果:
|
||||
|
||||
```bash
|
||||
npm run container:k6
|
||||
```
|
||||
|
||||
作品列表脚本一次 iteration 默认请求两个公开列表接口,因此目标 500 HTTP req/s 对应 `PEAK_RPS=250`:
|
||||
|
||||
```powershell
|
||||
$env:SCENARIO="spike"
|
||||
$env:START_RPS="25"
|
||||
$env:PEAK_RPS="250"
|
||||
$env:HOLD="60s"
|
||||
$env:END_RPS="25"
|
||||
$env:PREALLOCATED_VUS="100"
|
||||
$env:MAX_VUS="500"
|
||||
$env:DETAIL_RATIO="0"
|
||||
npm run container:k6
|
||||
```
|
||||
|
||||
容器内 `api-server` 资源上限与 Nginx 连接模型已经按 `genarrative-release` 的 2C / 2G / `nofile=4096` / `worker_connections=768` 收口;如果你要改成别的机器,就先重新采样再改这里。
|
||||
|
||||
SpacetimeDB 容器默认只提供运行时,不自动发布模块。首次启动或清理 `spacetime-data` 卷后,先只启动 `spacetimedb` 服务,再发布模块:
|
||||
|
||||
```bash
|
||||
npm run container:up -- spacetimedb
|
||||
spacetime publish genarrative-loadtest --server http://127.0.0.1:13101 --module-path server-rs/crates/spacetime-module --yes --build-options="--debug"
|
||||
```
|
||||
|
||||
发布完成后再执行 `npm run container:up` 和 `npm run container:k6`。如果 `deploy/container/api-server.env` 里的 `GENARRATIVE_SPACETIME_DATABASE` 改成了别的库名,发布命令里的库名也要同步修改。
|
||||
|
||||
如果要压 1000 HTTP req/s,把 `PEAK_RPS` 调到 `500`;如果要压 5000 HTTP req/s,把 `PEAK_RPS` 调到 `2500`,并同时提高 `PREALLOCATED_VUS` / `MAX_VUS`,观察是否先被带宽、Nginx `limit_conn` / `limit_req` 或 api-server 分组背压限制。当前容器 Nginx 对公开 gallery list 使用 `genarrative_gallery_rps`,公开详情和普通 API 使用 `genarrative_api_rps`,后台 API 使用 `genarrative_admin_rps`;api-server 侧对应 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS` 和 `GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS`。
|
||||
|
||||
2026-05-19 的 2C / 2G 容器压测结论:公开 gallery list 的 `limit_conn=320`、`limit_req rate=5000r/s burst=4096` 与 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320` 是当前发布口径。用宿主机 k6 打 `http://127.0.0.1:18080`,`PEAK_RPS=2500` 等价于约 5000 HTTP req/s 的两接口组合压测;连续 10 轮不重启 SpacetimeDB 的平均实际吞吐约 `4219 HTTP req/s`,总计 `1,897,357` 个 200、`212,542` 个 429、`0` 个 5xx,200 请求平均 `p95=123ms`、`p99=234ms`。该档会让 SpacetimeDB 内存从约 `366MiB` 累积到约 `885MiB / 896MiB`,下游内存先到危险区。当前不要为了降低“剩余 CPU”继续抬公开列表并发;下一步应减少成功列表请求后的 SpacetimeDB tracking 写入或优化下游连接 / 订阅状态,而不是放大入口并发。
|
||||
旧作品列表 / gallery 的 compose `k6` profile 与 `container:k6` 命令已经随模板业务退役。`scripts/loadtest/` 仅保留历史脚本和脱敏样例,不进入当前运行入口;新的容量验收必须针对现役编辑器、项目或素材接口单独设计,不能复用旧作品接口结论。
|
||||
|
||||
### 内存采样
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS=600
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS=900
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800
|
||||
GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512
|
||||
GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320
|
||||
GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64
|
||||
GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16
|
||||
GENARRATIVE_TRACKING_OUTBOX_ENABLED=true
|
||||
GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
|
||||
|
||||
@@ -118,7 +118,7 @@ services:
|
||||
soft: 4096
|
||||
hard: 4096
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/api/runtime/puzzle/gallery"]
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
@@ -139,32 +139,6 @@ services:
|
||||
- "${GENARRATIVE_CONTAINER_OTLP_GRPC_PORT:-4317}:4317"
|
||||
- "${GENARRATIVE_CONTAINER_OTLP_HTTP_PORT:-4318}:4318"
|
||||
|
||||
k6:
|
||||
image: grafana/k6:0.52.0
|
||||
profiles: ["loadtest"]
|
||||
cpus: "1.0"
|
||||
mem_limit: 512m
|
||||
depends_on:
|
||||
nginx:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
BASE_URL: http://nginx
|
||||
WORKS_DATA: data/works-list.sample.json
|
||||
SCENARIO: ${SCENARIO:-spike}
|
||||
START_RPS: ${START_RPS:-5}
|
||||
PEAK_RPS: ${PEAK_RPS:-250}
|
||||
HOLD: ${HOLD:-60s}
|
||||
END_RPS: ${END_RPS:-5}
|
||||
PREALLOCATED_VUS: ${PREALLOCATED_VUS:-100}
|
||||
MAX_VUS: ${MAX_VUS:-500}
|
||||
DETAIL_RATIO: ${DETAIL_RATIO:-0}
|
||||
SLEEP_MIN_SECONDS: ${SLEEP_MIN_SECONDS:-0}
|
||||
SLEEP_MAX_SECONDS: ${SLEEP_MAX_SECONDS:-0}
|
||||
volumes:
|
||||
- ../../scripts/loadtest:/scripts/loadtest:ro
|
||||
working_dir: /scripts/loadtest
|
||||
command: ["run", "k6-works-list.js"]
|
||||
|
||||
volumes:
|
||||
spacetime-data:
|
||||
api-tracking-outbox:
|
||||
|
||||
@@ -21,7 +21,6 @@ http {
|
||||
}
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_gallery_rps:10m rate=5000r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_api_rps:10m rate=300r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_admin_rps:10m rate=30r/s;
|
||||
|
||||
@@ -88,85 +87,6 @@ http {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /api/runtime/puzzle/gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location = /api/runtime/custom-world-gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/puzzle/gallery/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api(?:/|$) {
|
||||
default_type application/json;
|
||||
@@ -220,7 +140,7 @@ http {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" {
|
||||
location ~* "^/(?:creation|editor/canvas|profile|project)/?$" {
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
# END GENARRATIVE MAIN SPA ROUTES
|
||||
|
||||
Vendored
-5
@@ -18,8 +18,6 @@ GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS=600
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS=900
|
||||
GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800
|
||||
GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512
|
||||
GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320
|
||||
GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64
|
||||
GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16
|
||||
GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS=5000
|
||||
GENARRATIVE_TRACKING_OUTBOX_ENABLED=true
|
||||
@@ -77,9 +75,6 @@ GENARRATIVE_LLM_PROVIDER=openai-compatible
|
||||
GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=gpt-5.4-mini
|
||||
GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=false
|
||||
GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
|
||||
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## gzip
|
||||
|
||||
- `deploy/nginx/genarrative.conf` 与 `deploy/nginx/genarrative-dev-http.conf` 默认开启 gzip。
|
||||
- 覆盖 `application/json`,用于降低 `/api/runtime/*/gallery` 这类 JSON 列表接口的公网带宽占用。
|
||||
- 覆盖 `application/json`,用于降低 `/api/assets/history` 这类 JSON 列表接口的公网带宽占用。
|
||||
- 当前推荐等级为 `gzip_comp_level 5`,兼顾 2C/2G 服务器 CPU 与压缩收益。
|
||||
- Pingora 影子网关已通过 `GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip`、`GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=true`、`GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5` 与 `GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024` 承接 gzip parity;网关会把压缩算法收敛到 gzip allowlist,并按 `gzip_min_length 1024` 避免压缩小响应,同时沿用 Nginx `gzip_types` 边界不压缩图片资源。改动后必须运行 `npm run check:pingora-gateway-smoke` 验证小响应不压缩、图片资源不压缩,以及 `Accept-Encoding: gzip` 和 `Accept-Encoding: br, gzip` 的大响应都返回 gzip。
|
||||
|
||||
@@ -65,14 +65,14 @@
|
||||
## Pingora canary
|
||||
|
||||
- `deploy/nginx/snippets/genarrative-pingora-canary.conf` 是默认不启用的人工前缀 canary 入口,只在需要验证 Nginx -> Pingora handoff 时临时 include。它使用 `/__genarrative_pingora_canary/` 前缀改写后转发到 `127.0.0.1:18081`,并返回 `X-Genarrative-Nginx-Handoff: pingora-canary`。
|
||||
- `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 是前缀 canary 之后、direct 直连之前的真实路径 canary。它只能在 Nginx `http` 上下文人工 include,默认监听 `127.0.0.1:18083`,覆盖 `/api/creation-entry/config`、`/v1/identity`、`/v1/database/<db>/subscribe`、`/assets/app.js` 和拒绝入口,并写入独立 `/var/log/nginx/genarrative-pingora-realpath-canary.access.log`;不要把它 include 到生产 `443` server 内作为 location 覆盖。
|
||||
- `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 是前缀 canary 之后、direct 直连之前的真实路径 canary。它只能在 Nginx `http` 上下文人工 include,默认监听 `127.0.0.1:18083`,覆盖 `/api/assets/history`、`/v1/identity`、`/v1/database/<db>/subscribe`、`/assets/app.js` 和拒绝入口,并写入独立 `/var/log/nginx/genarrative-pingora-realpath-canary.access.log`;不要把它 include 到生产 `443` server 内作为 location 覆盖。
|
||||
- 真实路径 canary 片段使用 `access_log ... genarrative_upstream`,如果以独立文件放入 `/etc/nginx/conf.d/`,文件加载顺序必须晚于定义 `log_format genarrative_upstream` 的主站配置。当前目标机可使用 `/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf`;若站点把 `log_format` 移到全局 Nginx 配置,则需确保它仍在所有 `conf.d` server include 之前加载。启用前必须先跑 `nginx -t`,不要用会早于 `genarrative.conf` 加载的文件名。
|
||||
- 启用 Nginx canary 前,目标机必须已经有持久 `genarrative-pingora-gateway.service` 运行在 `127.0.0.1:18081`,且带真实 `api-server`、SpacetimeDB、静态目录和 `/var/log/genarrative/pingora-gateway.access.log` 完成本机 shadow 验收;不要从一次性 `/tmp` 网关进程直接切到 Nginx handoff。
|
||||
- 启用前必须运行 `npm run check:nginx-pingora-canary`;目标 agent 有 Nginx 时运行 `node scripts/check-nginx-pingora-canary.mjs --require-nginx`,同时做两个 snippet 的静态护栏和真实 `nginx -t`。本机或 CI 可运行 `npm run check:pingora-canary-docker`,用 Docker Nginx、真实 Pingora 和 mock 上游复现前缀 canary 与真实路径 canary 两条 handoff 链路。
|
||||
- 前缀 canary reload 后运行 `GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1 GENARRATIVE_PINGORA_CANARY_HOST=<域名> npm run check:pingora-canary-live`,再用 `scripts/check-pingora-canary-access-log-parity.mjs` 对账 `/var/log/nginx/genarrative.access.log` 与 `/var/log/genarrative/pingora-gateway.access.log`。
|
||||
- 真实路径 canary 不再手工编辑 `/etc/nginx/conf.d/`。目标机从 current release 执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083`;脚本会渲染到 `/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf`,保持晚于主站 `log_format genarrative_upstream` 加载,执行 `nginx -t`、reload Nginx,并默认运行 realpath live smoke。`nginx -t`、reload 或 live smoke 任一失败都会恢复写入前配置。关闭时执行 `/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply`,脚本会删除该 `zz-...` 文件,执行 `nginx -t` 和 reload,失败时恢复删除前配置。
|
||||
- `npm run check:pingora-release-readiness` 默认包含 `npm run check:pingora-realpath-canary-toggle`,用临时 Nginx 目录和 fake `nginx` / `systemctl` / live smoke 验证 realpath canary 启停脚本默认 dry-run、apply 写入、失败回滚和 disable 恢复逻辑;不再把这条脚本护栏留成单独可选项。
|
||||
- 真实路径 canary reload 后可再次运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/creation-entry/config --path /v1/identity --path /assets/app.js`。真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。
|
||||
- 真实路径 canary reload 后可再次运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再运行 `node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/assets/history --path /v1/identity --path /assets/app.js`。真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。
|
||||
- 正式切换前在源码 checkout / CI / 构建环境跑聚合门禁,并按目标机已启用的 canary 入口选择参数:前缀 canary 已启用时使用 `node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;真实路径 canary 已启用时追加或单独使用 `--require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`。如果现场只启用了真实路径 canary,不要同时传 `--require-live`。
|
||||
- 目标机 `/opt/genarrative/current` 只能跑随包 runtime-only 门禁,并同样按现场已启用入口选择:前缀 canary 用 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;真实路径 canary 用 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;验证 Pingora 直连入口时再追加 `--require-direct ...` 参数。未带 `--require-direct` 的 runtime-only 基础门禁会自动执行直连彩排状态复核,确认公网 `80/443` 仍由 Nginx 接流、Pingora shadow `127.0.0.1:18081`、realpath canary `127.0.0.1:18083` 和 current release 自审均通过;启用后 `--require-direct` 复核不再要求 Nginx 接公网彩排状态。生成正式直连切换 runbook 时,`--direct-redirect-host` 和回退 smoke Host 必须与 `--direct-host` 同 hostname,只允许端口不同。
|
||||
|
||||
@@ -84,11 +84,13 @@ ls -l /etc/nginx/modules-enabled/*brotli*
|
||||
nginx -T 2>/dev/null | grep -Ei 'brotli|load_module'
|
||||
|
||||
curl -sSI -H 'Accept-Encoding: gzip' \
|
||||
http://<host>/api/runtime/puzzle/gallery \
|
||||
-H 'Authorization: Bearer <access-token>' \
|
||||
'http://<host>/api/assets/history?kind=character_visual' \
|
||||
| grep -iE 'content-encoding|vary|content-type|content-length'
|
||||
|
||||
curl -sSI -H 'Accept-Encoding: br' \
|
||||
http://<host>/api/runtime/puzzle/gallery \
|
||||
-H 'Authorization: Bearer <access-token>' \
|
||||
'http://<host>/api/assets/history?kind=character_visual' \
|
||||
| grep -iE 'content-encoding|vary|content-type|content-length'
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ upstream genarrative_api {
|
||||
}
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_gallery_rps:10m rate=5000r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_api_rps:10m rate=300r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_admin_rps:10m rate=30r/s;
|
||||
|
||||
@@ -116,101 +115,6 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /api/runtime/puzzle/gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location = /api/runtime/custom-world-gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/puzzle/gallery/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
|
||||
location ~ ^/api(?:/|$) {
|
||||
@@ -285,7 +189,7 @@ server {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" {
|
||||
location ~* "^/(?:creation|editor/canvas|profile|project)/?$" {
|
||||
error_page 503 /maintenance.html;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
|
||||
@@ -12,7 +12,6 @@ upstream genarrative_api {
|
||||
}
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_gallery_rps:10m rate=5000r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_api_rps:10m rate=300r/s;
|
||||
limit_req_zone $binary_remote_addr zone=genarrative_admin_rps:10m rate=30r/s;
|
||||
|
||||
@@ -136,101 +135,6 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /api/runtime/puzzle/gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location = /api/runtime/custom-world-gallery {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 320;
|
||||
limit_req zone=genarrative_gallery_rps burst=4096 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/puzzle/gallery/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$ {
|
||||
default_type application/json;
|
||||
limit_conn genarrative_api_conn 32;
|
||||
limit_req zone=genarrative_api_rps burst=32 nodelay;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://genarrative_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
add_header X-Accel-Buffering no always;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
|
||||
location ~ ^/api(?:/|$) {
|
||||
@@ -305,7 +209,7 @@ server {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location ~* "^/(?:bark-battle|big-fish|child-motion-demo|creation|creation/baby-object-match|creation/baby-object-match/generating|creation/baby-object-match/result|creation/bark-battle|creation/bark-battle/generating|creation/bark-battle/result|creation/big-fish|creation/big-fish/generating|creation/big-fish/result|creation/creative-agent|creation/jump-hop|creation/jump-hop/generating|creation/jump-hop/result|creation/match3d|creation/match3d/generating|creation/match3d/result|creation/puzzle|creation/puzzle-clear|creation/puzzle-clear/generating|creation/puzzle-clear/result|creation/puzzle/generating|creation/puzzle/result|creation/rpg|creation/rpg/agent|creation/rpg/generating|creation/rpg/result|creation/square-hole|creation/square-hole/generating|creation/square-hole/result|creation/visual-novel|creation/visual-novel/generating|creation/visual-novel/result|creation/wooden-fish|creation/wooden-fish/generating|creation/wooden-fish/result|editor/canvas|gallery/jump-hop/detail|gallery/puzzle/detail|gallery/visual-novel/detail|match3d|project|puzzle|runtime/baby-love-drawing|runtime/baby-object-match|runtime/bark-battle|runtime/big-fish|runtime/jump-hop|runtime/match3d|runtime/puzzle|runtime/puzzle-clear|runtime/rpg/adventure|runtime/rpg/characters|runtime/square-hole|runtime/visual-novel|runtime/wooden-fish|works/detail|worlds/detail)/?$" {
|
||||
location ~* "^/(?:creation|editor/canvas|profile|project)/?$" {
|
||||
error_page 503 /maintenance.html;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
|
||||
@@ -26,7 +26,7 @@ server {
|
||||
proxy_pass http://127.0.0.1:18081/__genarrative_pingora/healthz;
|
||||
}
|
||||
|
||||
location = /api/creation-entry/config {
|
||||
location = /api/assets/history {
|
||||
default_type application/json;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"root /var/www/html;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/.well-known/acme-challenge/*`"]
|
||||
"docs": [
|
||||
"`/.well-known/acme-challenge/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "shadow_probe",
|
||||
@@ -24,7 +26,9 @@
|
||||
"expect": {
|
||||
"kind": "shadow_probe"
|
||||
},
|
||||
"docs": ["`/__genarrative_pingora/healthz`"]
|
||||
"docs": [
|
||||
"`/__genarrative_pingora/healthz`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "admin_redirect",
|
||||
@@ -34,10 +38,19 @@
|
||||
"location": "/admin/"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location = /admin", "return 301 /admin/;"],
|
||||
"development": ["location = /admin", "return 301 /admin/;"]
|
||||
"production": [
|
||||
"location = /admin",
|
||||
"return 301 /admin/;"
|
||||
],
|
||||
"development": [
|
||||
"location = /admin",
|
||||
"return 301 /admin/;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin`", "301 到 `/admin/`"]
|
||||
"docs": [
|
||||
"`/admin`",
|
||||
"301 到 `/admin/`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "admin_api_proxy",
|
||||
@@ -62,7 +75,9 @@
|
||||
"limit_req zone=genarrative_admin_rps burst=16 nodelay;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin/api/*`"]
|
||||
"docs": [
|
||||
"`/admin/api/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "admin_assets",
|
||||
@@ -73,10 +88,18 @@
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /admin/assets/", "try_files $uri =404;"],
|
||||
"development": ["location ^~ /admin/assets/", "try_files $uri =404;"]
|
||||
"production": [
|
||||
"location ^~ /admin/assets/",
|
||||
"try_files $uri =404;"
|
||||
],
|
||||
"development": [
|
||||
"location ^~ /admin/assets/",
|
||||
"try_files $uri =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin/assets/*`"]
|
||||
"docs": [
|
||||
"`/admin/assets/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "admin_spa_fallback",
|
||||
@@ -96,7 +119,9 @@
|
||||
"try_files $uri $uri/ /admin/index.html;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/admin/*`"]
|
||||
"docs": [
|
||||
"`/admin/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web_assets",
|
||||
@@ -106,115 +131,23 @@
|
||||
"root": "web",
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /assets/", "try_files $uri =404;"],
|
||||
"development": ["location ^~ /assets/", "try_files $uri =404;"]
|
||||
},
|
||||
"docs": ["`/assets/*`"]
|
||||
},
|
||||
{
|
||||
"id": "puzzle_gallery_list",
|
||||
"samplePath": "/api/runtime/puzzle/gallery",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_list"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location = /api/runtime/puzzle/gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
"location ^~ /assets/",
|
||||
"try_files $uri =404;"
|
||||
],
|
||||
"development": [
|
||||
"location = /api/runtime/puzzle/gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
"location ^~ /assets/",
|
||||
"try_files $uri =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/puzzle/gallery`"]
|
||||
},
|
||||
{
|
||||
"id": "custom_world_gallery_list",
|
||||
"samplePath": "/api/runtime/custom-world-gallery",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_list"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location = /api/runtime/custom-world-gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location = /api/runtime/custom-world-gallery",
|
||||
"limit_conn genarrative_api_conn 320;",
|
||||
"limit_req zone=genarrative_gallery_rps burst=4096 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/custom-world-gallery`"]
|
||||
},
|
||||
{
|
||||
"id": "puzzle_gallery_detail",
|
||||
"samplePath": "/api/runtime/puzzle/gallery/work-a",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_detail"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/api/runtime/puzzle/gallery/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/api/runtime/puzzle/gallery/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/puzzle/gallery/{id}`"]
|
||||
},
|
||||
{
|
||||
"id": "custom_world_gallery_detail",
|
||||
"samplePath": "/api/runtime/custom-world-gallery/profile-a/owner-b",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
"bodyLimit": null,
|
||||
"protectionClass": "gallery_detail"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
],
|
||||
"development": [
|
||||
"location ~ ^/api/runtime/custom-world-gallery/[^/]+/[^/]+$",
|
||||
"limit_conn genarrative_api_conn 32;",
|
||||
"limit_req zone=genarrative_api_rps burst=32 nodelay;",
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api/runtime/custom-world-gallery/{profile}/{owner}`"]
|
||||
"docs": [
|
||||
"`/assets/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "generic_api_proxy",
|
||||
"samplePath": "/api/creation-entry/config",
|
||||
"samplePath": "/api/assets/history",
|
||||
"expect": {
|
||||
"kind": "proxy",
|
||||
"target": "api",
|
||||
@@ -237,7 +170,9 @@
|
||||
"add_header X-Accel-Buffering no always;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/api`、`/api/*`"]
|
||||
"docs": [
|
||||
"`/api`、`/api/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spacetime_subscribe",
|
||||
@@ -264,7 +199,9 @@
|
||||
"proxy_read_timeout 3600s;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/v1/database/{db}/subscribe`"]
|
||||
"docs": [
|
||||
"`/v1/database/{db}/subscribe`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spacetime_identity",
|
||||
@@ -289,7 +226,9 @@
|
||||
"proxy_set_header Connection \"Upgrade\";"
|
||||
]
|
||||
},
|
||||
"docs": ["`/v1/identity*`"]
|
||||
"docs": [
|
||||
"`/v1/identity*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "v1_forbidden",
|
||||
@@ -298,10 +237,18 @@
|
||||
"kind": "not_found"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ^~ /v1/", "return 404;"],
|
||||
"development": ["location ^~ /v1/", "return 404;"]
|
||||
"production": [
|
||||
"location ^~ /v1/",
|
||||
"return 404;"
|
||||
],
|
||||
"development": [
|
||||
"location ^~ /v1/",
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/v1/*`"]
|
||||
"docs": [
|
||||
"`/v1/*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "healthz_forbidden",
|
||||
@@ -319,7 +266,9 @@
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/healthz*`"]
|
||||
"docs": [
|
||||
"`/healthz*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "readyz_forbidden",
|
||||
@@ -337,7 +286,9 @@
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/readyz*`"]
|
||||
"docs": [
|
||||
"`/readyz*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "generated_assets_forbidden",
|
||||
@@ -355,11 +306,13 @@
|
||||
"return 404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/generated-*`"]
|
||||
"docs": [
|
||||
"`/generated-*`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web_spa_fallback",
|
||||
"samplePath": "/creation/puzzle/result",
|
||||
"samplePath": "/project",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
@@ -375,21 +328,56 @@
|
||||
"try_files $uri /index.html =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["主站 SPA allowlist", "失败回退 `/index.html`"]
|
||||
"docs": [
|
||||
"主站 SPA allowlist",
|
||||
"失败回退 `/index.html`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web_spa_case_trailing_slash",
|
||||
"samplePath": "/CREATION/PUZZLE/RESULT/",
|
||||
"id": "profile_spa_fallback",
|
||||
"samplePath": "/profile",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "spa_fallback"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location ~*", "try_files $uri /index.html =404;"],
|
||||
"development": ["location ~*", "try_files $uri /index.html =404;"]
|
||||
"production": [
|
||||
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
||||
"try_files $uri /index.html =404;"
|
||||
],
|
||||
"development": [
|
||||
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
||||
"try_files $uri /index.html =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["大小写不敏感", "一个尾部斜杠"]
|
||||
"docs": [
|
||||
"主站 SPA allowlist",
|
||||
"失败回退 `/index.html`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web_spa_case_trailing_slash",
|
||||
"samplePath": "/PROJECT/",
|
||||
"expect": {
|
||||
"kind": "static",
|
||||
"root": "web",
|
||||
"mode": "spa_fallback"
|
||||
},
|
||||
"nginx": {
|
||||
"production": [
|
||||
"location ~*",
|
||||
"try_files $uri /index.html =404;"
|
||||
],
|
||||
"development": [
|
||||
"location ~*",
|
||||
"try_files $uri /index.html =404;"
|
||||
]
|
||||
},
|
||||
"docs": [
|
||||
"大小写不敏感",
|
||||
"一个尾部斜杠"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web_unknown_path_exact",
|
||||
@@ -400,10 +388,19 @@
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["location /", "try_files $uri $uri/ =404;"],
|
||||
"development": ["location /", "try_files $uri $uri/ =404;"]
|
||||
"production": [
|
||||
"location /",
|
||||
"try_files $uri $uri/ =404;"
|
||||
],
|
||||
"development": [
|
||||
"location /",
|
||||
"try_files $uri $uri/ =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["其它 Web 路径", "缺失时返回真实 404"]
|
||||
"docs": [
|
||||
"其它 Web 路径",
|
||||
"缺失时返回真实 404"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "creation_unknown_path_exact",
|
||||
@@ -414,10 +411,16 @@
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["try_files $uri $uri/ =404;"],
|
||||
"development": ["try_files $uri $uri/ =404;"]
|
||||
"production": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
],
|
||||
"development": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/creation/not-exist`"]
|
||||
"docs": [
|
||||
"`/creation/not-exist`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime_unknown_path_exact",
|
||||
@@ -428,10 +431,16 @@
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["try_files $uri $uri/ =404;"],
|
||||
"development": ["try_files $uri $uri/ =404;"]
|
||||
"production": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
],
|
||||
"development": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/runtime/not-exist`"]
|
||||
"docs": [
|
||||
"`/runtime/not-exist`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "puzzle_unknown_path_exact",
|
||||
@@ -442,10 +451,16 @@
|
||||
"mode": "exact"
|
||||
},
|
||||
"nginx": {
|
||||
"production": ["try_files $uri $uri/ =404;"],
|
||||
"development": ["try_files $uri $uri/ =404;"]
|
||||
"production": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
],
|
||||
"development": [
|
||||
"try_files $uri $uri/ =404;"
|
||||
]
|
||||
},
|
||||
"docs": ["`/puzzle/not-exist`"]
|
||||
"docs": [
|
||||
"`/puzzle/not-exist`"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,12 +62,6 @@ GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT=64
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND=30
|
||||
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST=16
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT=320
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND=5000
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST=4096
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT=32
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND=300
|
||||
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST=32
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT=64
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND=300
|
||||
GENARRATIVE_PINGORA_GATEWAY_API_BURST=64
|
||||
|
||||
@@ -503,6 +503,9 @@
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
},
|
||||
"409": {
|
||||
"$ref": "#/components/responses/Conflict"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1327,6 +1330,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conflict": {
|
||||
"description": "画布 revision 冲突,需重新读取最新快照后合并或重试",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpstreamError": {
|
||||
"description": "上游生成服务失败",
|
||||
"content": {
|
||||
@@ -1755,8 +1768,17 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object",
|
||||
"description": "画布图层 JSON,最大约 256KB。"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"description": "兼容画布布局 JSON,最大 2 MiB;服务端会拆分为结构化图层与生成对话框行。"
|
||||
},
|
||||
"expectedRevision": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "可选的画布 revision CAS;不匹配时返回 409。"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2110,7 +2132,11 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
@@ -2132,6 +2158,8 @@
|
||||
"title",
|
||||
"viewport",
|
||||
"layers",
|
||||
"revision",
|
||||
"layoutStorageVersion",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
@@ -2149,7 +2177,25 @@
|
||||
"$ref": "#/components/schemas/EditorCanvasViewport"
|
||||
},
|
||||
"layers": {
|
||||
"type": "object"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"layoutStorageVersion": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
|
||||
@@ -26,6 +26,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-19 角色动作视频使用单进程批量抽帧
|
||||
|
||||
- 背景:角色动作生成在拿到预览视频后,原实现会为 `32 / 40 / 48` 个采样点分别启动一次 FFmpeg、重复解码同一视频。release 的 2 vCPU 主机在一次 32 帧任务中因此出现约 10 秒的 CPU 尖刺,且进程启动和重复解码都不是业务必需开销。
|
||||
- 决策:角色动作抽帧必须先沿用 `compute_sample_time_seconds()` 计算全部采样点,再通过一个 FFmpeg filter graph 对输入统一 `setpts`、`split`,各分支按精确 `select=gte(t\,<target>)` 输出一帧。不得改用会漂移现有采样时刻的粗粒度 `fps` 抽帧。单次命令完成后逐一确认全部目标文件存在,任一缺帧继续使用原有用户错误文案,并在 details 中保留首个缺帧的 `targetSeconds / outputPath`、整批 `missingFrames` 和 stdout/stderr。视频封面等单帧调用保留兼容 helper,但内部复用同一批量实现。
|
||||
- 影响范围:`server-rs/crates/api-server/src/character_animation_assets.rs` 的角色动作视频本地抽帧和单帧视频封面抽取;不改变尾帧安全步长、BgFilter 并发、OSS 路径、帧编号、透明化后处理或前后端结果契约。
|
||||
- 验证方式:运行 `cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`,真实短视频回归必须由一次 FFmpeg 命令产出整批帧,并继续断言 `32帧·4秒` 最后一帧为 `3.875s`;追加 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`、Rust 格式、编码和 diff 门禁。
|
||||
- 关联文档:`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-18 图片生成 K 档由 provider 直接生成
|
||||
|
||||
- 背景:旧 gpt-image-2 尺寸表会把 2K 竖版回落到 `1024x1536`,图标入口又使用固定 `360x360 / 512x512` 占位;角色去背景结果变小时还会直接放大整张透明成品,导致 UI 显示的 2K 与模型实际生成清晰度不一致。
|
||||
@@ -750,6 +760,7 @@
|
||||
- 2026-06-20 桌面能力清单单测边界:Tauri `capabilities.rs` 必须用 Rust 单测同时覆盖桌面 runtime capability 清单顺序、无重复、真实桌面能力完整包含,并显式排除 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact` 等未接入能力;桌面单端配置检查会反查该测试边界,避免只靠方案文档或共享 profile 发现桌面壳能力伪声明。
|
||||
- 2026-06-20 桌面本地通知契约镜像:Tauri `notification.showLocal` 的 title / body 归一化、长度上限和成功结果 action 必须镜像共享 HostBridge 契约;Rust 侧常量使用 `HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH`、`HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH` 和 `HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION` 命名,桌面单端配置检查会与 `packages/shared/src/contracts/hostBridge.ts` 比对数值并反查成功结果由该 action 常量组装,避免通知 payload 边界变成桌面壳本地规则。
|
||||
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
|
||||
> 2026-07-18 覆盖说明:本段后续关于微信 `navigation.openNativePage`、生成结果订阅页、`[subscribe-message]` 日志和订阅页路由门禁的 2026-06 决策均已由旧创作模板退役决策废止,只作为历史记录。Expo / Tauri 的同源 H5 受控导航及微信登录、支付、分享能力继续有效。
|
||||
- 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()` 在 `native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。
|
||||
- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。
|
||||
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示受控分享动作,并按 `hostShell` 区分 Expo 系统分享面板和 Tauri 剪贴板复制表达,避免旧壳或裁剪壳露出不可用入口。
|
||||
@@ -878,6 +889,8 @@
|
||||
|
||||
## 2026-06-17 H5 宿主壳能力统一走 HostBridge
|
||||
|
||||
> 2026-07-18 覆盖说明:以下订阅授权、订阅页和旧玩法导航部分已退役;登录、支付、分享、九宫切图与通用 HostBridge 分层仍有效。
|
||||
|
||||
- 背景:主站同时运行在普通浏览器、微信小程序 `web-view` 和未来可能出现的原生 App WebView 中;登录、支付、分享、订阅授权和运行态分享目标同步曾散落在业务组件与服务文件里,后续新增宿主壳会导致同一业务重复分叉。
|
||||
- 决策:前端宿主运行态识别、微信小程序 JS SDK 加载、原生页跳转、支付跳转、登录跳转、九宫切图和 `postMessage` 统一收口到 `src/services/host-bridge/hostBridge.ts`,业务层优先调用 `getHostRuntime`、`requestHostLogin`、`requestHostPayment`、`navigateHostNativePage`、`setHostShareTarget` 和 `openHostShareGrid`。`authService`、分享服务、订阅授权和个人中心充值可保留兼容导出或业务编排,但不再自行加载微信 JS SDK 或直接判断 `wx.miniProgram`。固定内置玩法不走代码包下载流程;AI 生成 H5 沙箱后续单独定义受限 `GameBridge`,不得直接暴露完整 `HostBridge`。
|
||||
- 影响范围:`src/services/host-bridge/`、`src/services/authService.ts`、`src/services/payment/paymentPlatform.ts`、`src/services/wechatMiniProgramShareGrid.ts`、`src/services/wechatMiniProgramShareTarget.ts`、`src/services/wechatMiniProgramSubscribe.ts`、`src/components/platform-entry/usePlatformProfileCenterController.ts`、微信小程序壳和未来原生 App 壳接入。
|
||||
@@ -4317,3 +4330,27 @@
|
||||
- 决策:旧创作模板、旧创作入口及其专属运行服务进入下线范围,不再为跳一跳、抓大鹅 Match3D、儿童动作 Demo 等旧链路修复兼容问题、补生成脚本或维持专属门禁。
|
||||
- 边界:共享账号、钱包、资产、图片编辑器、公开作品、通用 HostBridge、API、SpacetimeDB、发布运维和安全能力不属于旧链路,仍需维持正式门禁。历史文档只作为背景材料,不再作为旧入口继续运行的依据。
|
||||
- 清理方式:允许直接删除已经失效的旧素材生成命令、入口路由、专属服务和对应测试;删除工程链路时仍需核对是否被当前共享能力引用,不能连带移除仍在使用的公共契约或持久化事实。
|
||||
- 最终落地:本次退役范围覆盖整个旧创作模板体系,包括 RPG / 自定义世界、拼图、拼消消、大鱼吃小鱼、敲木鱼、方洞挑战、视觉小说、汪汪声浪、寓教于乐、Creative Agent、Match3D、跳一跳和儿童动作 Demo。全部相关历史表继续作为数据壳参与 `spacetime-module` 编译,`migration.rs` 白名单与历史数据不变;旧 reducer/procedure/view、API 路由/handler/worker、前端页面/工作台/运行态、共享业务 DTO 和纯业务 crate 从编译链与依赖图移除,但旧源码和素材保留在仓库中用于历史追溯。
|
||||
- 兼容读取:只保留历史审计、迁移和资产归属核对所需的最小读取定义;旧 `worldType`、公开作品号、URL、详情页和专属运行态均不再形成用户可访问入口。
|
||||
- 方案文档:`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
|
||||
|
||||
## 2026-07-18 恢复现役平台公共壳但禁止旧业务依赖回流
|
||||
|
||||
- 背景:旧创作模板退役时误把新版 `/creation`、桌面公共侧边栏和“我的”完整资料页一起缩减;只恢复视觉后,现役 profile client 又经 `rpg-entry` barrel 把旧作品库、旧 runtime request 和展示模型重新带入 Vite 与 TypeScript 图。
|
||||
- 决策:桌面端继续使用原平台公共结构,一级导航固定为 `创作 / 项目 / 我的`;顶栏保留编辑器项目 / 素材搜索、泥点入口和账号胶囊;“我的”全宽保留资料编辑、陶泥号、三项统计、充值、兑换码、社区、反馈、通用设置、API Key 和法律信息。搜索只面向编辑器项目与公开编辑器素材,不恢复旧公开作品搜索。
|
||||
- 依赖边界:公共 dashboard、钱包、充值、兑换码、邀请码、API Key 和设置请求迁入 `services/platform-entry`,公共账单展示迁入现役 profile model。Vite 新增退役模块 graph 门禁,ESLint 对现役源码禁止导入旧目录;目录 watch ignore、Tailwind source、tsconfig include 和 tree-shaking 都不能作为依赖隔离证明。
|
||||
- 路由与响应式边界:`/creation`、`/project`、`/profile` 都是可刷新、可前进 / 后退的稳定路由;桌面端使用侧边栏,移动端必须提供同样 `创作 / 项目 / 我的` 的三项底部 dock,不得因隐藏桌面侧边栏而丢失移动导航。
|
||||
- 公共设置边界:`runtime_setting` 保持原表结构与历史数据,但它是音乐音量和平台主题的现役账号级公共能力,不归入旧玩法数据壳。鉴权后的 `GET/PUT /api/runtime/settings` 必须经 `spacetime-client` 调用 `get_runtime_setting_or_default` / `upsert_runtime_setting_and_return`;保留该路由不构成恢复旧 runtime API 的先例。
|
||||
- 编译门禁:除旧业务目录外,`src/uiAssets.ts`、`src/types.ts`、`src/types/**`、`src/services/runtimeAudioFeedback.ts` 和 `src/services/publicWorkCode.ts` 也是顶层退役 module,必须同时退出 Vite module graph、TypeScript、ESLint 和 Vitest;`/audio/**`、`/chat.png`、`/fusion-pixel.ttf` 及旧 pixel / story-tab / 玩法 CSS 不得进入 dev 服务或生产产物。验收时必须同时检查 `tsc --listFilesOnly`、Vite 依赖图 / 产物和退役资产路径,不能只依赖 tree-shaking。
|
||||
- Rust 产物边界:`module-runtime` 继续承载账号、钱包、公共设置、追踪和 feature gate,但 `CreationEntry*`、旧公开作品、存档、浏览历史与游玩统计 DTO / command / mapper / 规则必须退出实际 rlib;只保留历史表需要的 `RuntimeBrowseHistoryThemeMode`、完整保序的钱包流水来源枚举等持久化 ABI。`check:server-rs-ddd` 必须执行 `check:module-runtime-artifact`,同时验证旧符号和字面量为零、必要 ABI 仍存在,不能以源码存在 `#[cfg(any())]` 或路由未挂载代替产物证明。
|
||||
- 外围编译边界:`platform-auth` 不再编译 runtime guest token,`platform-wechat` 不再编译旧玩法生成结果订阅消息,小程序不再注册订阅授权页;旧公开作品资产授权 view 退出 SpacetimeDB module,匿名素材读取只保留现役 editor showcase 派生授权。
|
||||
- 历史队列边界:现役 external generation worker 只领取 `source_module = editor-canvas` 的任务,历史旧玩法 pending / running 行保持原状态,不得被新 worker 领取后改写为失败。
|
||||
- Agent crate 边界:`platform-agent` 的执行器、工具注册表、回调和拼图 Phase 1 输入均属于已退役 Creative Agent 业务,不得因现役编辑器 Agent 共用一个模型名常量而留在 workspace 或 `api-server` 依赖图。该常量收口到 `platform-llm`,`platform-agent` 与仅由它引入的 `langchainrust` 退出在运 Cargo resolve graph,源码目录继续仅作历史追溯。
|
||||
- 防回流补充:顶层 `creationEntryConfigService`、`creationUrlState`、`customWorld*`、`runtimeGuestAuth`、`runtimeRequest`、`input-devices`、`useCombatFlow`、`useStoryOptions`、`useMocapInput` 和微信生成订阅 facade 同样属于退役前端模块;Vite dev 对旧 `/api/creation*` 与 `/api/public-works*` 前缀直接返回 404,不能回落 SPA HTML。
|
||||
- Vite 全量边界补充:`src/games/**`、`src/data/**`、`src/prompts/**`、旧顶层 App / Playground、旧路由和 `services/ai.ts` 必须由 pre-transform 门禁直接拒绝;所有同源 `/generated-*` 裸读在 dev 与生产统一为空 `404`,历史对象只经现役签名读取接口兼容,不允许 SPA fallback 伪装成资产成功响应。
|
||||
- 前端混合根目录补充:`src/components`、`src/hooks`、`src/persistence`、`src/routing`、`src/services` 的根级文件实行现役白名单,Vite 与 ESLint 使用同一口径阻断旧 RPG / 玩法根文件;子目录仍按现役目录和退役目录分别管理,新增公共根文件必须显式登记。
|
||||
- 影响范围:`PlatformEntryActiveFlowShell`、`PlatformActiveProfileView`、编辑器 / 项目搜索、平台 profile clients、`module-runtime`、`platform-llm`、Cargo workspace / resolve graph、Vite / ESLint / Rust 产物门禁及旧业务退役方案。
|
||||
|
||||
## 2026-07-20 VectorEngine 图片任务预算收口到 worker deadline
|
||||
|
||||
- 决策:`editor_image_generation`、`editor_image_edit`、`editor_icon_spritesheet_generation` 和 `editor_ui_design_asset_extraction` 使用默认 `1800s` long job 预算。worker 从同一起点计算绝对 job deadline,并向 provider 提前保留 `min(60s, job 预算 / 2)` 作为审计、OSS 和终态写回窗口。deadline 只经进程内 `RequestContext` 传递;VectorEngine 单 attempt 取配置 timeout 与剩余预算的较小值,退避加下一次 attempt 无法落在同一 deadline 内时停止重试,参考图和响应图片下载也受同一 deadline 限制。普通 HTTP / `inline` 保持无 deadline 行为;`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 默认仍为 `1000000`,配置加载层允许显式值更低。lease 续租 / fencing、迟到写回仲裁、attempt 耗尽和原子退款语义不变。
|
||||
|
||||
@@ -186,7 +186,7 @@ npm run check:rustfmt
|
||||
cargo fmt --all --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。
|
||||
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;现役账号、钱包、编辑器、项目、精选素材和公共设置路由按 `tracking.rs` 的显式静态路由表记录,后台路由默认排除。旧玩法、公开作品和专属运行态路由已经退役,不再维护作品级游玩埋点覆盖。
|
||||
|
||||
编码检查:
|
||||
|
||||
@@ -231,23 +231,15 @@ npm run check:native-shells
|
||||
```
|
||||
|
||||
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 EAS 原生包构建 profile、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口、H5 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、Tauri `Info.plist`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件,但不扫描 Expo export、Tauri `target/`、Cargo / Metro 缓存或 release 构建产物。移动壳配置检查必须反查 EAS 生产 profile、文本 / 文档 / 图片 / 音频导入边界都来自共享 HostBridge 契约。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts` 和 `src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时;H5 业务调用链允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
|
||||
创作 Agent 原生壳文档导入优先走 `file.importDocument`,旧壳只声明 `file.importText` 时才回退文本导入;相关变更必须让根级和单端门禁覆盖共享 method、capability profile、文档 MIME / 5 MiB 上限、读取前 size 校验,以及 H5 base64 转 `File` 后继续走后端文档解析的链路。
|
||||
创作 Agent 参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后继续交给既有 `onReferenceImageChange` 校验链路;用户取消原生选择不应再连带弹出浏览器文件输入,普通浏览器、小程序和未声明能力的裁剪壳才使用原隐藏文件输入。
|
||||
创作 Agent 轻输入 composer 的参考图按钮在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄参考图入口,并把宿主图片同样转为 `File` 后复用 `readPuzzleReferenceImageAsDataUrl` 的类型、大小、压缩和预览链路。
|
||||
反馈页上传凭证在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄凭证入口,并把拍摄图片同样转为 `File` 后复用反馈页原有数量、大小、MIME、data URL 预览和提交 payload 校验。
|
||||
固定内置 H5 体验入口在原生壳声明 `navigation.openNativePage` 时必须优先走 `navigateHostNativePage()`;例如儿童动作热身 Demo 从平台首页进入 `/child-motion-demo` 时应由 HostBridge 发出 `navigation.openNativePage`,宿主不可用时才回退浏览器跳转。
|
||||
Expo / Tauri 声明 `navigation.openNativePage` 时,只用于现役同源 H5 路由的受控导航和宿主上下文续接;微信小程序不再声明该能力。旧儿童动作 Demo、模板工作台、生成页、结果页和运行态不得作为 HostBridge 导航验收入口。
|
||||
H5 支付链接跳转在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实支付 SDK 前不得声明 `payment.request`,也不得把外部 H5 支付跳转伪装成原生支付成功。
|
||||
微信 OAuth 登录授权 URL 在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实登录 SDK 前不得声明 `auth.requestLogin`,也不得把网页登录跳转伪装成原生登录成功。
|
||||
汪汪声浪结果页玩家 / 对手 / UI 背景三图槽位上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后继续交给 `uploadBarkBattleAsset` 与当前槽位写回链路;用户取消原生选择不应再连带弹出浏览器文件输入,普通浏览器、小程序和未声明能力的裁剪壳才使用原隐藏文件输入。
|
||||
抓大鹅结果页发布封面图和封面参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有封面 data URL 读取、AI 重绘开关、参考图集合和封面生成 payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
|
||||
RPG 角色资产工作室的角色参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readFileAsDataUrl` 参考图集合和角色形象生成 payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
|
||||
RPG 作品封面上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 10 MiB 校验、图片尺寸读取、16:9 裁剪和 `uploadCustomWorldCoverImage` 保存链路;用户取消原生选择不应再连带弹出浏览器文件输入。
|
||||
RPG 作品封面参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readImageFileAsDataUrl` 读取、预览和 `generateCustomWorldCoverImage` payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
|
||||
RPG 场景图片参考图上传在原生壳声明 `file.importImage` 时必须优先走宿主图片导入,并把 H5 base64 转 `File` 后复用现有 `readImageFileAsDataUrl` 读取、预览和 `rpgCreationAssetClient.generateSceneImage` payload 链路;用户取消原生选择不应再连带弹出浏览器文件输入。
|
||||
现役编辑器与个人中心新增文件导入能力时,继续复用共享 `file.importDocument`、`file.importImage`、`file.captureImage` 和 MIME / 大小门禁,不得把已退役模板的上传 service、玩法 DTO 或页面测试重新纳入原生壳门禁。
|
||||
视觉小说结果页封面 / 角色 / 场景图片和音乐 / 环境音上传在原生壳声明 `file.importImage` / `file.importAudio` 时必须优先走宿主受控导入,并把 H5 base64 转 `File` 后继续交给 `uploadVisualNovelAsset` 与当前素材字段写回链路;用户取消原生选择不应再连带弹出浏览器文件输入,历史素材选择和 AI 图片生成保持原链路。
|
||||
该命令会反查微信小程序 `WECHAT_HOST_CAPABILITIES` 与共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES` 一致;小程序生产代码继续保留 CommonJS 运行时镜像,不直接 import TypeScript shared 包。
|
||||
该命令同时会运行微信小程序 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的壳层测试,保证微信桥接层拆分后的支付、订阅消息、九宫切图、分享目标和 WebView 登录 / 分享入口行为与 Expo、Tauri 壳一起验收。
|
||||
该命令还会反查微信小程序 `app.json.pages` 与 `host-bridge/protocol.js` 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY`、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段和 H5 / API base URL 格式,避免页面路由、来源标记、宿主上下文 query 或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。
|
||||
该命令还会反查微信小程序 `app.json.pages` 与 `host-bridge/protocol.js` 现役页面 URL、H5 小程序页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY`、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段和 H5 / API base URL 格式,避免页面路由、来源标记、宿主上下文 query 或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。旧生成结果订阅授权页和对应 H5 service 不再进入清单。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。
|
||||
|
||||
内容检查:
|
||||
|
||||
@@ -314,8 +306,8 @@ npm run check:server-rs-ddd
|
||||
|
||||
- 移动端优先,再兼容网页端。
|
||||
- 页面只展示后端返回的状态,不自行计算结论型业务状态。
|
||||
- 创作中心入口配置事实源在 SpacetimeDB,通过 `GET /api/creation-entry/config` 下发;前端只在 `platformEntryCreationTypes.ts` 做展示派生,api-server 路由熔断也使用同一份配置,禁止恢复前端硬编码入口配置文件。底部加号创作入口页公告位也跟随后端 `eventBanners` 配置,前端只做展示和轮播;后台公告用表单维护标题与 HTML 内容,保存时再序列化为后端 `eventBannersJson` 传输字段。`最近创作` 不属于模板分类,不能作为分类缺失兜底;生成中和生成失败的真实草稿摘要都应进入最近创作。
|
||||
- 一期统一创作页字段 spec 同样跟随 `GET /api/creation-entry/config`,由 `creationTypes[].unifiedCreationSpec` 下发;拼图、抓大鹅、敲木鱼之外的模板不接入该扩展位,前端只保留旧后端缺字段时的兜底默认。
|
||||
- 现役一级入口为 `/creation`、`/project`、`/profile`,桌面侧边栏和移动端底部 dock 都固定显示“创作 / 项目 / 我的”。`/creation` 只读取图片编辑器项目与 `GET /api/editor/showcase/resources`,不得重新接入旧模板入口配置、旧作品架或专属运行态。
|
||||
- 旧创作模板目录和顶层旧业务模块必须持续退出 Vite、TypeScript、ESLint 与 Vitest;旧 `/api/creation-entry/config`、模板 API、公开作品详情和运行态 API 必须保持未挂载。SpacetimeDB 历史表、迁移白名单与必要兼容类型只作为数据壳保留,不得据此恢复业务逻辑。
|
||||
- 优先复用现有面板、抽屉、弹窗,不新建独立大系统。
|
||||
- 不在 UI 中默认写功能说明类文本。
|
||||
- 弹出独立面板的交互不要实现成在当前面板下方追加内容。
|
||||
@@ -329,7 +321,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
## 提交前建议让 Agent 执行
|
||||
|
||||
涉及拼图、抓大鹅、敲木鱼统一创作 / 生成链路、Phase 2 之后的跨玩法回归或本地 dev 栈时,先按 `quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md` 和对应单项门禁文档执行自动脚本与体验检查。
|
||||
涉及现役编辑器、项目、精选素材、账号、钱包、公共设置或本地 dev 栈时,按对应定向测试、类型检查、Rust / SpacetimeDB 门禁、真实浏览器 smoke 与本文件当前命令验收。旧跨玩法回归和单玩法质量门禁只作历史记录,不再作为提交前现役检查入口。
|
||||
|
||||
```text
|
||||
请检查当前 git diff,指出:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user