Increase VectorEngine timeouts and add image UI

Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
This commit is contained in:
2026-05-15 02:40:59 +08:00
parent 4642855fd0
commit 74fd9a33ac
87 changed files with 5508 additions and 1261 deletions

View File

@@ -587,6 +587,49 @@ describe('apiClient', () => {
});
});
it('prefers api error details.reason over details.message for diagnostics', async () => {
setStoredAccessToken('details-reason-first-token', { emit: false });
fetchMock.mockResolvedValueOnce(
createResponseMock({
status: 502,
body: JSON.stringify({
ok: false,
data: null,
error: {
code: 'UPSTREAM_ERROR',
message: '上游暂不可用',
details: {
provider: 'vector-engine',
message:
'创建拼图 VectorEngine 图片编辑任务失败error sending request for url (https://api.vectorengine.ai/v1/images/edits)',
reason:
'无法连接 VectorEngine 图片编辑接口请检查服务器网络、DNS、防火墙或代理配置',
endpoint: 'https://api.vectorengine.ai/v1/images/edits',
},
},
meta: {},
}),
headers: {
'Content-Type': 'application/json',
},
}),
);
await expect(
requestJson('/api/runtime/puzzle/agent/sessions/test/actions', {
method: 'POST',
}, '执行拼图操作失败。'),
).rejects.toMatchObject({
message:
'无法连接 VectorEngine 图片编辑接口请检查服务器网络、DNS、防火墙或代理配置',
status: 502,
code: 'UPSTREAM_ERROR',
details: {
provider: 'vector-engine',
},
});
});
it('uses api error details.reason when details.message is absent', async () => {
setStoredAccessToken('details-reason-token', { emit: false });
fetchMock.mockResolvedValueOnce(

View File

@@ -95,7 +95,7 @@ describe('babyDrawingClient', () => {
}),
'生成宝贝爱画魔法图片失败',
expect.objectContaining({
timeoutMs: 180000,
timeoutMs: 1_000_000,
}),
);
});

View File

@@ -16,6 +16,7 @@ import { type ApiRetryOptions, requestJson } from '../apiClient';
const STORAGE_KEY = 'genarrative.edutainmentBabyDrawing.localDrawings.v1';
const BABY_LOVE_DRAWING_MAGIC_API =
'/api/creation/edutainment/baby-love-drawing/magic';
const BABY_LOVE_DRAWING_MAGIC_TIMEOUT_MS = 1_000_000;
const BABY_LOVE_DRAWING_MAGIC_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 800,
@@ -116,7 +117,7 @@ export async function createBabyLoveDrawingMagicImage(
'生成宝贝爱画魔法图片失败',
{
retry: BABY_LOVE_DRAWING_MAGIC_RETRY,
timeoutMs: 180000,
timeoutMs: BABY_LOVE_DRAWING_MAGIC_TIMEOUT_MS,
},
);
}

View File

@@ -153,7 +153,7 @@ describe('babyObjectMatchClient', () => {
signal: expect.any(AbortSignal),
}),
);
expect(BABY_OBJECT_MATCH_ASSET_REQUEST_TIMEOUT_MS).toBe(600_000);
expect(BABY_OBJECT_MATCH_ASSET_REQUEST_TIMEOUT_MS).toBe(1_000_000);
expect(response.draft.itemAssets[0]).toMatchObject({
itemId: 'baby-object-item-1',
itemName: '苹果',

View File

@@ -23,7 +23,7 @@ import { buildBabyObjectMatchPublicWorkCode } from '../publicWorkCode';
const STORAGE_KEY = 'genarrative.edutainmentBabyObject.localDrafts.v1';
const BABY_OBJECT_MATCH_ASSET_API =
'/api/creation/edutainment/baby-object-match/assets';
export const BABY_OBJECT_MATCH_ASSET_REQUEST_TIMEOUT_MS = 600_000;
export const BABY_OBJECT_MATCH_ASSET_REQUEST_TIMEOUT_MS = 1_000_000;
const BABY_OBJECT_MATCH_ASSET_REQUEST_RETRY: ApiRetryOptions = {
maxRetries: 0,
};

View File

@@ -21,6 +21,7 @@ import { type ApiRetryOptions, requestJson } from '../apiClient';
const MATCH3D_WORKS_API_BASE = '/api/creation/match3d/works';
const MATCH3D_GALLERY_API_BASE = '/api/runtime/match3d/gallery';
const VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS = 1_000_000;
const MATCH3D_WORKS_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
@@ -152,7 +153,7 @@ export function generateMatch3DCoverImage(
'生成抓大鹅封面图失败',
{
retry: MATCH3D_WORKS_WRITE_RETRY,
timeoutMs: 240_000,
timeoutMs: VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
},
);
}
@@ -174,7 +175,7 @@ export function generateMatch3DBackgroundImage(
'生成抓大鹅背景图失败',
{
retry: MATCH3D_WORKS_WRITE_RETRY,
timeoutMs: 240_000,
timeoutMs: VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
},
);
}
@@ -196,7 +197,7 @@ export function generateMatch3DContainerImage(
'生成抓大鹅容器形象失败',
{
retry: MATCH3D_WORKS_WRITE_RETRY,
timeoutMs: 240_000,
timeoutMs: VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
},
);
}

View File

@@ -1,5 +1,5 @@
/**
* 平台首页资料读取入口。
* 直连 RPG profile client避免默认首页首访经过服务桶入口触发额外模块转译
* 复用 RPG profile 聚合出口,避免平台入口和 RPG 入口测试、鉴权包装出现两套读取口径
*/
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry/rpgProfileClient';
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry';

View File

@@ -0,0 +1,94 @@
const PUZZLE_HISTORY_ASSET_FALLBACK_NAME = '历史拼图素材';
function safeDecodePathSegment(value: string) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function parsePuzzleHistoryTimestamp(value: string) {
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const directDate = new Date(trimmed);
if (!Number.isNaN(directDate.getTime())) {
return directDate;
}
const numericMatch = /^(-?\d+)(?:\.(\d{1,9}))?Z?$/u.exec(trimmed);
if (!numericMatch) {
return null;
}
const wholeSeconds = Number.parseInt(numericMatch[1] ?? '', 10);
if (!Number.isFinite(wholeSeconds)) {
return null;
}
const fractionalMillis = Number.parseInt(
(numericMatch[2] ?? '').padEnd(3, '0').slice(0, 3) || '0',
10,
);
const normalizedMillis = Number.isFinite(fractionalMillis)
? fractionalMillis
: 0;
const useMilliseconds =
Math.abs(wholeSeconds) >= 100_000_000_000 ||
(numericMatch[1] ?? '').length > 10;
const timestampMs = useMilliseconds
? wholeSeconds + (wholeSeconds < 0 ? -normalizedMillis : normalizedMillis)
: wholeSeconds * 1000 +
(wholeSeconds < 0 ? -normalizedMillis : normalizedMillis);
return new Date(timestampMs);
}
export function getPuzzleHistoryAssetDisplayName(
imageSrc: string | null | undefined,
) {
const trimmed = imageSrc?.trim() ?? '';
if (!trimmed) {
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
const pathOnly = trimmed.split(/[?#]/u)[0]?.trim() ?? '';
if (!pathOnly) {
return PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
const fileName = pathOnly.replace(/^\/+/u, '').split('/').filter(Boolean).pop();
const displayName = safeDecodePathSegment(fileName ?? '').trim();
return displayName || PUZZLE_HISTORY_ASSET_FALLBACK_NAME;
}
export function formatPuzzleHistoryAssetCreatedAt(value: string) {
const parsedDate = parsePuzzleHistoryTimestamp(value);
if (!parsedDate) {
return '未知时间';
}
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(parsedDate);
}
export function getPuzzleHistoryAssetReferenceLabel(
imageSrc: string | null | undefined,
) {
const displayName = getPuzzleHistoryAssetDisplayName(imageSrc);
if (displayName === PUZZLE_HISTORY_ASSET_FALLBACK_NAME) {
return '历史素材';
}
return `历史素材 · ${displayName}`;
}