Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
export const USER_MESSAGE_KEYS = {
|
||||
unknown: "messages.common.unknown",
|
||||
network: "messages.network.unavailable",
|
||||
badRequest: "messages.http.bad_request",
|
||||
unauthorized: "messages.http.unauthorized",
|
||||
forbidden: "messages.http.forbidden",
|
||||
notFound: "messages.http.not_found",
|
||||
conflict: "messages.http.conflict",
|
||||
payloadTooLarge: "messages.http.payload_too_large",
|
||||
timeout: "messages.http.timeout",
|
||||
tooManyRequests: "messages.http.too_many_requests",
|
||||
server: "messages.http.server",
|
||||
promptRequired: "messages.validation.prompt_required",
|
||||
authLoginRequired: "messages.auth.login_required",
|
||||
authCodeInvalid: "messages.auth.code_invalid",
|
||||
authCodeExpired: "messages.auth.code_expired",
|
||||
authInvalidCredentials: "messages.auth.invalid_credentials",
|
||||
authInvalidRegion: "messages.auth.invalid_region",
|
||||
authProviderNotReady: "messages.auth.provider_not_ready",
|
||||
authHumanVerificationRequired: "messages.auth.human_verification_required",
|
||||
authHumanVerificationFailed: "messages.auth.human_verification_failed"
|
||||
} as const;
|
||||
|
||||
const HTTP_STATUS_MESSAGE_KEYS: Record<number, string> = {
|
||||
400: USER_MESSAGE_KEYS.badRequest,
|
||||
401: USER_MESSAGE_KEYS.unauthorized,
|
||||
403: USER_MESSAGE_KEYS.forbidden,
|
||||
404: USER_MESSAGE_KEYS.notFound,
|
||||
408: USER_MESSAGE_KEYS.timeout,
|
||||
409: USER_MESSAGE_KEYS.conflict,
|
||||
413: USER_MESSAGE_KEYS.payloadTooLarge,
|
||||
429: USER_MESSAGE_KEYS.tooManyRequests
|
||||
};
|
||||
|
||||
const BACKEND_STATUS_MESSAGE_KEYS: Record<string, string> = {
|
||||
"auth.login_required": USER_MESSAGE_KEYS.authLoginRequired,
|
||||
"auth.code_invalid": USER_MESSAGE_KEYS.authCodeInvalid,
|
||||
"auth.code_expired": USER_MESSAGE_KEYS.authCodeExpired,
|
||||
"auth.invalid_credentials": USER_MESSAGE_KEYS.authInvalidCredentials,
|
||||
"auth.invalid_region": USER_MESSAGE_KEYS.authInvalidRegion,
|
||||
"auth.provider_not_ready": USER_MESSAGE_KEYS.authProviderNotReady,
|
||||
"auth.human_verification_required": USER_MESSAGE_KEYS.authHumanVerificationRequired,
|
||||
"auth.human_verification_failed": USER_MESSAGE_KEYS.authHumanVerificationFailed
|
||||
};
|
||||
|
||||
const RAW_STATUS_MESSAGE_KEYS: Array<{ pattern: RegExp; key: string }> = [
|
||||
{ pattern: /verification code is invalid/i, key: USER_MESSAGE_KEYS.authCodeInvalid },
|
||||
{ pattern: /verification code is expired/i, key: USER_MESSAGE_KEYS.authCodeExpired },
|
||||
{ pattern: /login required/i, key: USER_MESSAGE_KEYS.authLoginRequired },
|
||||
{ pattern: /prompt is required/i, key: USER_MESSAGE_KEYS.promptRequired }
|
||||
];
|
||||
|
||||
export type BackendStatus = string | number;
|
||||
|
||||
export type BackendStatusBody = {
|
||||
error?: string;
|
||||
message?: string;
|
||||
errorCode?: string;
|
||||
code?: number;
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
};
|
||||
|
||||
export type RequestDomain = "auth" | "design" | "api";
|
||||
|
||||
export type Translate = (key: string, values?: Record<string, string | number>) => string;
|
||||
|
||||
export class ApiFailure extends Error {
|
||||
status: number;
|
||||
backendStatus: BackendStatus;
|
||||
userMessageKey: string;
|
||||
rawMessage: string;
|
||||
body: unknown;
|
||||
domain: RequestDomain;
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
|
||||
constructor(domain: RequestDomain, status: number, statusText: string, body: unknown) {
|
||||
const parsed = parseStatusBody(body);
|
||||
const backendStatus = parsed.errorCode ?? parsed.code ?? status;
|
||||
const userMessageKey = resolveUserMessageKey(backendStatus, status, parsed.rawMessage);
|
||||
super(userMessageKey);
|
||||
this.name = "ApiFailure";
|
||||
this.domain = domain;
|
||||
this.status = status;
|
||||
this.backendStatus = backendStatus;
|
||||
this.userMessageKey = userMessageKey;
|
||||
this.rawMessage = parsed.rawMessage || statusText;
|
||||
this.body = body;
|
||||
this.requestId = parsed.requestId;
|
||||
this.traceId = parsed.traceId;
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkFailure extends Error {
|
||||
backendStatus = "network.unavailable";
|
||||
userMessageKey = USER_MESSAGE_KEYS.network;
|
||||
domain: RequestDomain;
|
||||
cause?: unknown;
|
||||
|
||||
constructor(domain: RequestDomain, cause?: unknown) {
|
||||
super(USER_MESSAGE_KEYS.network);
|
||||
this.name = "NetworkFailure";
|
||||
this.domain = domain;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiFailure(domain: RequestDomain, response: Response, body: unknown) {
|
||||
return new ApiFailure(domain, response.status, response.statusText, body);
|
||||
}
|
||||
|
||||
export function createNetworkFailure(domain: RequestDomain, cause?: unknown) {
|
||||
return new NetworkFailure(domain, cause);
|
||||
}
|
||||
|
||||
export function backendStatusOf(value: unknown): BackendStatus | undefined {
|
||||
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
|
||||
return value.backendStatus;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function userMessageKeyOf(value: unknown) {
|
||||
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
|
||||
return value.userMessageKey;
|
||||
}
|
||||
return USER_MESSAGE_KEYS.unknown;
|
||||
}
|
||||
|
||||
export function httpStatusOf(value: unknown) {
|
||||
return value instanceof ApiFailure ? value.status : undefined;
|
||||
}
|
||||
|
||||
export function isBackendStatus(value: unknown, status: BackendStatus) {
|
||||
return backendStatusOf(value) === status;
|
||||
}
|
||||
|
||||
export function isUserMessageKey(value: unknown, key: string) {
|
||||
return userMessageKeyOf(value) === key;
|
||||
}
|
||||
|
||||
export function toUserMessage(value: unknown, t: Translate, fallbackKey: string = USER_MESSAGE_KEYS.unknown) {
|
||||
const key = userMessageKeyOf(value);
|
||||
const message = t(key);
|
||||
if (message !== key) return message;
|
||||
return t(fallbackKey);
|
||||
}
|
||||
|
||||
function parseStatusBody(body: unknown) {
|
||||
if (!body || typeof body !== "object") {
|
||||
return {};
|
||||
}
|
||||
const data = body as BackendStatusBody;
|
||||
return {
|
||||
rawMessage: stringValue(data.message) || stringValue(data.error),
|
||||
errorCode: stringValue(data.errorCode),
|
||||
code: typeof data.code === "number" ? data.code : undefined,
|
||||
requestId: stringValue(data.requestId),
|
||||
traceId: stringValue(data.traceId)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveUserMessageKey(backendStatus: BackendStatus, httpStatus: number, rawMessage: string | undefined) {
|
||||
if (typeof backendStatus === "string" && BACKEND_STATUS_MESSAGE_KEYS[backendStatus]) {
|
||||
return BACKEND_STATUS_MESSAGE_KEYS[backendStatus];
|
||||
}
|
||||
for (const item of RAW_STATUS_MESSAGE_KEYS) {
|
||||
if (item.pattern.test(rawMessage ?? "")) return item.key;
|
||||
}
|
||||
if (typeof backendStatus === "number" && HTTP_STATUS_MESSAGE_KEYS[backendStatus]) {
|
||||
return HTTP_STATUS_MESSAGE_KEYS[backendStatus];
|
||||
}
|
||||
if (HTTP_STATUS_MESSAGE_KEYS[httpStatus]) {
|
||||
return HTTP_STATUS_MESSAGE_KEYS[httpStatus];
|
||||
}
|
||||
return httpStatus >= 500 ? USER_MESSAGE_KEYS.server : USER_MESSAGE_KEYS.unknown;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user