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:
@@ -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(
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('babyDrawingClient', () => {
|
||||
}),
|
||||
'生成宝贝爱画魔法图片失败',
|
||||
expect.objectContaining({
|
||||
timeoutMs: 180000,
|
||||
timeoutMs: 1_000_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: '苹果',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
94
src/services/puzzle-works/puzzleHistoryAsset.ts
Normal file
94
src/services/puzzle-works/puzzleHistoryAsset.ts
Normal 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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user