Merge branch 'master' into editor-agent-right-click

# Conflicts:
#	src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx
This commit is contained in:
2026-07-21 10:41:23 +08:00
1066 changed files with 139112 additions and 32290 deletions
-7
View File
@@ -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
View File
@@ -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',
},
+28 -128
View File
@@ -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';
}
-141
View File
@@ -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;
+34 -62
View File
@@ -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>
);
+3 -11
View File
@@ -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>;
+7 -24
View File
@@ -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 回落到第一项', () => {
+19 -23
View File
@@ -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 =
+8 -1
View File
@@ -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"
]
}
+6 -39
View File
@@ -10,11 +10,10 @@ Docker Compose
├─ nginx :80 -> api-server:8082,负责静态站点、/admin/、/api/ 反代、upstream timing log、连接限制
├─ api-server :8082Linux release 构建,连接 compose 内 SpacetimeDB
├─ external-generation-worker,独立 worker 进程,消费 external_generation_job 队列
─ otelcol :4317/4318debug exporter,接收 traces / metrics / logs
└─ k6 profile=loadtest 时临时启动,在 compose 网络内压 nginx
─ otelcol :4317/4318debug 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` 个 5xx200 请求平均 `p95=123ms``p99=234ms`。该档会让 SpacetimeDB 内存从约 `366MiB` 累积到约 `885MiB / 896MiB`,下游内存先到危险区。当前不要为了降低“剩余 CPU”继续抬公开列表并发;下一步应减少成功列表请求后的 SpacetimeDB tracking 写入或优化下游连接 / 订阅状态,而不是放大入口并发。
旧作品列表 / gallery 的 compose `k6` profile 与 `container:k6` 命令已经随模板业务退役。`scripts/loadtest/` 仅保留历史脚本和脱敏样例,不进入当前运行入口;新的容量验收必须针对现役编辑器、项目或素材接口单独设计,不能复用旧作品接口结论。
### 内存采样
-2
View File
@@ -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
+1 -27
View File
@@ -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:
+1 -81
View File
@@ -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
-5
View File
@@ -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
+7 -5
View File
@@ -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'
```
+1 -97
View File
@@ -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) {
+1 -97
View File
@@ -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;
+153 -138
View File
@@ -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 错误回传给 H5WebView 拦截保持 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