refactor(*): 代码质量 - 优化前后端实现并完善协议与画布验证

This commit is contained in:
ddcat666
2026-10-08 19:38:57 +08:00
parent 9be3aa8235
commit 87ecb3000c
724 changed files with 3046 additions and 3885 deletions

View File

@@ -1,301 +1,218 @@
import axios from "axios";
import { nanoid } from "nanoid";
import { consumeJsonEvents } from "@/lib/json-event-stream";
import type {
ChatCompletionPayload,
ChatCompletionStreamState,
GeminiPart,
GeminiPayload,
GeminiStreamState,
ImageApiResponse,
ResponseApiOutputItem,
ResponseApiPayload,
ResponseStreamState,
ResponseToolCall,
ToolResponseResult,
ChatCompletionPayload, ChatCompletionStreamState, GeminiPart, GeminiPayload, GeminiStreamState,
ImageApiResponse, ResponseApiPayload, ResponseStreamState, ResponseToolCall, ToolResponseResult,
} from "@/services/api/image-contracts";
function resolveImageDataUrl(item: Record<string, unknown>) {
if (typeof item.b64_json === "string" && item.b64_json) return `data:image/png;base64,${item.b64_json}`;
if (typeof item.url === "string" && item.url) return item.url;
return null;
type TextSink = (text: string) => void;
type TextState = { text: string; reasoning: string };
function record(value: unknown): Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
export function parseImagePayload(payload: ImageApiResponse) {
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
const images =
payload.data
?.map(resolveImageDataUrl)
.filter((value): value is string => Boolean(value))
.map((dataUrl) => ({ id: nanoid(), dataUrl })) || [];
if (images.length === 0) throw new Error("接口没有返回图片");
return images;
}
export function readAxiosError(error: unknown, fallback: string) {
if (axios.isCancel(error)) return "请求已取消";
if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
const responseData = error.response?.data;
return responseData?.msg || responseData?.error?.message || readStatusError(error.response?.status, fallback);
}
if (error instanceof DOMException && error.name === "AbortError") return "请求已取消";
return error instanceof Error ? error.message : fallback;
}
export function readStatusError(status: number | undefined, fallback: string) {
if (status === 401 || status === 403) return "鉴权失败,请检查 API Key、套餐权限或模型权限";
if (status === 429) return "请求被限流或额度不足,请稍后重试";
return status ? `${fallback}:${status}` : fallback;
}
export function parseChatCompletionPayload(payload: ChatCompletionPayload): ToolResponseResult {
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
if (payload.error?.message) throw new Error(payload.error.message);
const message = payload.choices?.[0]?.message;
const toolCalls = (message?.tool_calls || [])
.map((call) => ({
id: call.id || "",
type: "function" as const,
function: { name: call.function?.name || "", arguments: call.function?.arguments || "{}" },
}))
.filter((call) => call.id && call.function.name);
return { content: message?.content || "", toolCalls, ...(message?.reasoning_content ? { reasoning: message.reasoning_content } : {}) };
}
export function parseToolResponse(payload: ResponseApiPayload): ToolResponseResult {
const output = payload.output || [];
const content =
payload.output_text ||
output
.flatMap((item) => (item.type === "message" ? item.content || [] : []))
.map((item) => item.text || "")
.join("");
const toolCalls = output
.filter((item): item is Extract<ResponseApiOutputItem, { type?: "function_call" }> => item.type === "function_call")
.map((item) => ({
id: item.call_id || item.id || "",
type: "function" as const,
function: { name: item.name || "", arguments: item.arguments || "{}" },
}))
.filter((item) => item.id && item.function.name);
return { content, toolCalls };
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function stringValue(value: unknown) {
function text(value: unknown): string {
return typeof value === "string" ? value : "";
}
export function responseErrorMessage(value: unknown) {
if (!isRecord(value)) return "";
const error = isRecord(value.error) ? value.error : undefined;
const response = isRecord(value.response) ? value.response : undefined;
const responseError = response && isRecord(response.error) ? response.error : undefined;
return stringValue(value.msg) || stringValue(error?.message) || stringValue(responseError?.message);
}
export function validateResponsePayload(payload: ResponseApiPayload) {
function assertEnvelope(payload: { code?: number; msg?: string }) {
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
if (payload.error?.message) throw new Error(payload.error.message);
}
export function validateGeminiPayload(payload: GeminiPayload) {
if (payload.error?.message) throw new Error(payload.error.message);
if (payload.promptFeedback?.blockReason) throw new Error(`Gemini 拒绝了本次请求:${payload.promptFeedback.blockReason}`);
function imagesFrom(sources: Iterable<string | undefined | null>, emptyMessage: string) {
const images: { id: string; dataUrl: string }[] = [];
for (const dataUrl of sources) if (dataUrl) images.push({ id: nanoid(), dataUrl });
if (!images.length) throw new Error(emptyMessage);
return images;
}
export async function readFetchError(response: Response, fallback: string) {
const text = await response.text();
if (!text) return readStatusError(response.status, fallback);
try {
return responseErrorMessage(JSON.parse(text)) || readStatusError(response.status, fallback);
} catch {
return text.slice(0, 300) || readStatusError(response.status, fallback);
}
function addTool(result: ToolResponseResult, id: string | undefined, name: string | undefined, args: string | undefined, signature?: string) {
if (!id || !name) return;
const call: ResponseToolCall = { type: "function", id, function: { name, arguments: args || "{}" } };
if (signature) call.thoughtSignature = signature;
result.toolCalls.push(call);
}
export async function readJsonPayload<T>(response: Response, fallback: string): Promise<T> {
const text = await response.text();
try {
return JSON.parse(text) as T;
} catch {
if (/^\s*(?:<!doctype|<html)/i.test(text)) throw new Error("后端代理返回了前端网页,请检查 VITE_CANVAS_BACKEND_URL 和反向代理配置");
throw new Error(`${fallback}:接口没有返回有效 JSON`);
}
function* geminiParts(payload: GeminiPayload): Generator<GeminiPart> {
for (const candidate of payload.candidates ?? []) yield* candidate.content?.parts ?? [];
}
function parseEventBlock(block: string) {
const data = block
.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, ""))
.join("\n")
.trim();
return data && data !== "[DONE]" ? data : undefined;
export function parseImagePayload(payload: ImageApiResponse) {
assertEnvelope(payload);
function* sources() {
for (const item of payload.data ?? []) {
const encoded = text(item.b64_json);
yield encoded ? `data:image/png;base64,${encoded}` : text(item.url);
}
}
return imagesFrom(sources(), "接口没有返回图片");
}
function consumeResponseStreamBlock(block: string, state: ResponseStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
const data = parseEventBlock(block);
if (!data) return;
const event = JSON.parse(data) as Record<string, unknown>;
const type = stringValue(event.type);
const errorMessage = responseErrorMessage(event);
if (errorMessage) state.error = errorMessage;
if (type === "response.output_text.delta" && typeof event.delta === "string") {
state.text += event.delta;
onDelta?.(state.text);
}
if (type === "response.output_text.done" && !state.text && typeof event.text === "string") {
state.text = event.text;
onDelta?.(state.text);
}
if (["response.reasoning.delta", "response.reasoning_text.delta", "response.reasoning_summary_text.delta"].includes(type) && typeof event.delta === "string") {
state.reasoning += event.delta;
onReasoning?.(state.reasoning);
}
if (["response.reasoning.done", "response.reasoning_text.done", "response.reasoning_summary_text.done"].includes(type) && !state.reasoning && typeof event.text === "string") {
state.reasoning = event.text;
onReasoning?.(state.reasoning);
}
if (type === "response.completed" && isRecord(event.response)) state.payload = event.response as ResponseApiPayload;
else if (Array.isArray(event.output)) state.payload = event as ResponseApiPayload;
export function parseChatCompletionPayload(payload: ChatCompletionPayload): ToolResponseResult {
validateResponsePayload(payload);
const message = payload.choices?.[0]?.message;
const result: ToolResponseResult = { content: message?.content || "", toolCalls: [] };
if (message?.reasoning_content) result.reasoning = message.reasoning_content;
for (const call of message?.tool_calls ?? []) addTool(result, call.id, call.function?.name, call.function?.arguments);
return result;
}
export function consumeResponseStreamText(state: ResponseStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
state.buffer += text;
for (;;) {
const match = state.buffer.match(/\r?\n\r?\n/);
if (!match) break;
const index = match.index ?? 0;
consumeResponseStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
state.buffer = state.buffer.slice(index + match[0].length);
}
if (flush && state.buffer.trim()) {
consumeResponseStreamBlock(state.buffer, state, onDelta, onReasoning);
state.buffer = "";
}
}
function consumeChatCompletionStreamBlock(block: string, state: ChatCompletionStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
const data = parseEventBlock(block);
if (!data) return;
const event = JSON.parse(data) as Record<string, unknown>;
const errorMessage = responseErrorMessage(event);
if (errorMessage) state.error = errorMessage;
const choices = Array.isArray(event.choices) ? event.choices : [];
const choice = isRecord(choices[0]) ? choices[0] : undefined;
const delta = choice && isRecord(choice.delta) ? choice.delta : undefined;
if (!delta) return;
if (typeof delta.content === "string") {
state.text += delta.content;
onDelta?.(state.text);
}
const reasoningDelta = stringValue(delta.reasoning_content) || stringValue(delta.reasoning) || stringValue(delta.reasoning_text);
if (reasoningDelta) {
state.reasoning += reasoningDelta;
onReasoning?.(state.reasoning);
}
const chunks = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
chunks.forEach((value, fallbackIndex) => {
if (!isRecord(value)) return;
const callIndex = typeof value.index === "number" ? value.index : fallbackIndex;
const current = state.toolCalls.get(callIndex) || { id: "", name: "", arguments: "" };
const fn = isRecord(value.function) ? value.function : undefined;
state.toolCalls.set(callIndex, {
id: stringValue(value.id) || current.id,
name: stringValue(fn?.name) || current.name,
arguments: current.arguments + stringValue(fn?.arguments),
});
});
}
export function consumeChatCompletionStreamText(state: ChatCompletionStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
state.buffer += text;
for (;;) {
const match = state.buffer.match(/\r?\n\r?\n/);
if (!match) break;
const index = match.index ?? 0;
consumeChatCompletionStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
state.buffer = state.buffer.slice(index + match[0].length);
}
if (flush && state.buffer.trim()) {
consumeChatCompletionStreamBlock(state.buffer, state, onDelta, onReasoning);
state.buffer = "";
}
}
function consumeGeminiStreamBlock(block: string, state: GeminiStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
const data = parseEventBlock(block);
if (!data) return;
const result = parseGeminiToolResponse(JSON.parse(data) as GeminiPayload);
if (result.reasoning) {
state.reasoning += result.reasoning;
onReasoning?.(state.reasoning);
}
if (result.content) {
state.text += result.content;
onDelta?.(state.text);
}
state.toolCalls.push(...result.toolCalls);
}
export function consumeGeminiStreamText(state: GeminiStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
state.buffer += text;
for (;;) {
const match = state.buffer.match(/\r?\n\r?\n/);
if (!match) break;
const index = match.index ?? 0;
consumeGeminiStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
state.buffer = state.buffer.slice(index + match[0].length);
}
if (flush && state.buffer.trim()) {
consumeGeminiStreamBlock(state.buffer, state, onDelta, onReasoning);
state.buffer = "";
export function parseToolResponse(payload: ResponseApiPayload): ToolResponseResult {
const result: ToolResponseResult = { content: "", toolCalls: [] };
for (const item of payload.output ?? []) {
switch (item.type) {
case "message":
for (const part of item.content ?? []) result.content += part.text || "";
break;
case "function_call":
addTool(result, item.call_id || item.id, item.name, item.arguments);
break;
}
}
if (payload.output_text) result.content = payload.output_text;
return result;
}
export function parseGeminiToolResponse(payload: GeminiPayload): ToolResponseResult {
validateGeminiPayload(payload);
const parts = payload.candidates?.flatMap((candidate) => candidate.content?.parts || []) || [];
const content = parts
.filter((part) => !part.thought)
.map((part) => part.text || "")
.join("");
const reasoning = parts
.filter((part) => part.thought)
.map((part) => part.text || "")
.join("");
const toolCalls = parts
.map((part) => part.functionCall)
.filter((call): call is NonNullable<GeminiPart["functionCall"]> => Boolean(call?.name))
.map((call) => {
const part = parts.find((item) => item.functionCall === call);
const thoughtSignature = part?.thoughtSignature || part?.thought_signature;
return {
id: call.id || nanoid(),
type: "function" as const,
function: { name: call.name || "", arguments: JSON.stringify(call.args || {}) },
...(thoughtSignature ? { thoughtSignature } : {}),
};
});
return { content, toolCalls, ...(reasoning ? { reasoning } : {}) };
const result: ToolResponseResult = { content: "", toolCalls: [] };
let reasoning = "";
for (const part of geminiParts(payload)) {
if (part.thought) reasoning += part.text || "";
else result.content += part.text || "";
const call = part.functionCall;
if (call?.name) addTool(result, call.id || nanoid(), call.name, JSON.stringify(call.args || {}), part.thoughtSignature || part.thought_signature);
}
if (reasoning) result.reasoning = reasoning;
return result;
}
export function parseGeminiImagePayload(payload: GeminiPayload) {
validateGeminiPayload(payload);
const images =
payload.candidates
?.flatMap((candidate) => candidate.content?.parts || [])
.map((part) => {
const inlineData = part.inlineData || (part.inline_data ? { mimeType: part.inline_data.mimeType || part.inline_data.mime_type, data: part.inline_data.data } : undefined);
if (inlineData?.data) return `data:${inlineData.mimeType || "image/png"};base64,${inlineData.data}`;
return part.fileData?.fileUri || null;
})
.filter((value): value is string => Boolean(value))
.map((dataUrl) => ({ id: nanoid(), dataUrl })) || [];
if (!images.length) throw new Error("Gemini 接口没有返回图片");
return images;
function* sources() {
for (const part of geminiParts(payload)) {
const inline = part.inlineData ?? part.inline_data;
const mime = part.inlineData ? part.inlineData.mimeType : part.inline_data?.mimeType || part.inline_data?.mime_type;
yield inline?.data ? `data:${mime || "image/png"};base64,${inline.data}` : part.fileData?.fileUri;
}
}
return imagesFrom(sources(), "Gemini 接口没有返回图片");
}
export function responseErrorMessage(value: unknown) {
const payload = record(value);
return text(payload.msg) || text(record(payload.error).message) || text(record(record(payload.response).error).message);
}
export function validateResponsePayload(payload: ResponseApiPayload) {
assertEnvelope(payload);
if (payload.error?.message) throw new Error(payload.error.message);
}
export function validateGeminiPayload(payload: GeminiPayload) {
const failure = payload.error?.message || (payload.promptFeedback?.blockReason ? `Gemini 拒绝了本次请求:${payload.promptFeedback.blockReason}` : "");
if (failure) throw new Error(failure);
}
export function readStatusError(status: number | undefined, fallback: string) {
const known: Record<number, string> = {
401: "鉴权失败,请检查 API Key、套餐权限或模型权限",
403: "鉴权失败,请检查 API Key、套餐权限或模型权限",
429: "请求被限流或额度不足,请稍后重试",
};
return status ? known[status] || `${fallback}:${status}` : fallback;
}
export function readAxiosError(error: unknown, fallback: string) {
if (axios.isCancel(error) || (error instanceof DOMException && error.name === "AbortError")) return "请求已取消";
if (!axios.isAxiosError(error)) return error instanceof Error ? error.message : fallback;
const payload = record(error.response?.data);
return text(payload.msg) || text(record(payload.error).message) || readStatusError(error.response?.status, fallback);
}
export async function readFetchError(response: Response, fallback: string) {
const body = await response.text();
const status = readStatusError(response.status, fallback);
if (!body) return status;
let payload: unknown;
try { payload = JSON.parse(body); } catch { return body.slice(0, 300) || status; }
return responseErrorMessage(payload) || status;
}
export async function readJsonPayload<T>(response: Response, fallback: string): Promise<T> {
const body = await response.text();
try { return JSON.parse(body) as T; } catch {
const detail = /^\s*(?:<!doctype|<html)/i.test(body)
? "后端代理返回了前端网页,请检查 VITE_CANVAS_BACKEND_URL 和反向代理配置"
: `${fallback}:接口没有返回有效 JSON`;
throw new Error(detail);
}
}
function updateText(state: TextState, field: keyof TextState, value: unknown, sink: TextSink | undefined, final = false) {
if (typeof value !== "string" || (final && state[field])) return;
state[field] = final ? value : state[field] + value;
sink?.(state[field]);
}
const responseChannels: Record<string, keyof TextState> = {
"response.output_text": "text",
"response.reasoning": "reasoning",
"response.reasoning_text": "reasoning",
"response.reasoning_summary_text": "reasoning",
};
export function consumeResponseStreamText(state: ResponseStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
consumeJsonEvents(state, chunk, (payload) => {
const event = record(payload);
const failure = responseErrorMessage(event);
if (failure) state.error = failure;
const type = text(event.type);
const separator = type.lastIndexOf(".");
const field = Object.hasOwn(responseChannels, type.slice(0, separator)) ? responseChannels[type.slice(0, separator)] : undefined;
const action = type.slice(separator + 1);
if (field && (action === "delta" || action === "done")) {
updateText(state, field, action === "done" ? event.text : event.delta, field === "text" ? onDelta : onReasoning, action === "done");
}
if (type === "response.completed" && event.response !== null && typeof event.response === "object" && !Array.isArray(event.response)) {
state.payload = event.response as ResponseApiPayload;
} else if (Array.isArray(event.output)) state.payload = event as ResponseApiPayload;
}, flush);
}
export function consumeChatCompletionStreamText(state: ChatCompletionStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
consumeJsonEvents(state, chunk, (payload) => {
const event = record(payload);
const failure = responseErrorMessage(event);
if (failure) state.error = failure;
const first = Array.isArray(event.choices) ? record(event.choices[0]) : {};
const delta = record(first.delta);
updateText(state, "text", delta.content, onDelta);
const reasoning = text(delta.reasoning_content) || text(delta.reasoning) || text(delta.reasoning_text);
if (reasoning) updateText(state, "reasoning", reasoning, onReasoning);
if (!Array.isArray(delta.tool_calls)) return;
for (const [fallback, raw] of delta.tool_calls.entries()) {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
const call = record(raw);
const index = typeof call.index === "number" ? call.index : fallback;
const previous = state.toolCalls.get(index);
const fn = record(call.function);
state.toolCalls.set(index, {
id: text(call.id) || previous?.id || "",
name: text(fn.name) || previous?.name || "",
arguments: (previous?.arguments || "") + text(fn.arguments),
});
}
}, flush);
}
export function consumeGeminiStreamText(state: GeminiStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
consumeJsonEvents(state, chunk, (payload) => {
const result = parseGeminiToolResponse(payload as GeminiPayload);
if (result.reasoning) updateText(state, "reasoning", result.reasoning, onReasoning);
if (result.content) updateText(state, "text", result.content, onDelta);
state.toolCalls.push(...result.toolCalls);
}, flush);
}

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import { readFileAsDataUrl } from "@/lib/image-utils";
import type { ApiEnvelope, ApiVideoResponse, RequestOptions, SeedanceTask, VideoGenerationResult } from "./video-contracts";
@@ -54,29 +55,23 @@ export async function assertVideoBlob(blob: Blob) {
export function delay(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new DOMException("Aborted", "AbortError"));
return;
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
},
{ once: true },
);
let timer: ReturnType<typeof setTimeout> | undefined;
const finish = (aborted: boolean) => {
clearTimeout(timer);
signal?.removeEventListener("abort", cancel);
if (aborted) reject(new DOMException("Aborted", "AbortError"));
else resolve();
};
const cancel = () => finish(true);
if (signal?.aborted) { cancel(); return; }
signal?.addEventListener("abort", cancel, { once: true });
timer = setTimeout(() => finish(false), ms);
});
}
export function blobToDataUrl(blob: Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("读取本地素材失败"));
reader.readAsDataURL(blob);
});
export async function blobToDataUrl(blob: Blob) {
try { return await readFileAsDataUrl(blob); }
catch { throw new Error("读取本地素材失败"); }
}
export async function videoResultFromUrl(url: string, options?: RequestOptions): Promise<VideoGenerationResult> {

View File

@@ -6,8 +6,8 @@ import { withCanvasStorePersistenceLock, type CanvasProject } from "@/stores/can
import { withGenerationArtifactCommitLock } from "@/services/generation-asset-repository";
export type CanvasSyncDraft = { id: string; savedAt: string; project: CanvasProject };
const draftKey = (projectId: string) => `infinite-canvas:sync-drafts:${projectId}`;
const draftIndexKey = "infinite-canvas:sync-draft-index";
const draftKey = (projectId: string) => `yingce:sync-drafts:${projectId}`;
const draftIndexKey = "yingce:sync-draft-index";
export async function readAllCanvasSyncDrafts(scope = getActiveUserScope()) {
const raw = await localForageStorageForScope(scope).getItem(draftIndexKey);

View File

@@ -26,7 +26,7 @@ export type UploadedFile = {
remoteUploadError?: string;
};
const store = localforage.createInstance({ name: "infinite-canvas", storeName: "media_files" });
const store = localforage.createInstance({ name: "yingce", storeName: "media_files" });
const objectUrls = new Map<string, string>();
export async function uploadMediaFile(input: Blob, prefix = "file", onProgress?: (uploadedBytes: number, totalBytes: number) => void): Promise<UploadedFile> {

View File

@@ -11,8 +11,8 @@ type GenerationStorageLockOptions = {
requireCrossRealmLock?: boolean;
};
const ASSET_STORAGE_LOCK_PREFIX = "infinite-canvas:generation-asset-storage-lock:";
const ARTIFACT_COMMIT_LOCK_PREFIX = "infinite-canvas:generation-artifact-commit-lock:";
const ASSET_STORAGE_LOCK_PREFIX = "yingce:generation-asset-storage-lock:";
const ARTIFACT_COMMIT_LOCK_PREFIX = "yingce:generation-artifact-commit-lock:";
const assetStorageTails = new Map<string, Promise<void>>();
const artifactCommitTails = new Map<string, Promise<void>>();

View File

@@ -23,7 +23,7 @@ export type UploadedImage = {
remoteUploadError?: string;
};
const store = localforage.createInstance({ name: "infinite-canvas", storeName: "image_files" });
const store = localforage.createInstance({ name: "yingce", storeName: "image_files" });
const objectUrls = new Map<string, string>();
export async function uploadImage(input: string | Blob, onProgress?: (uploadedBytes: number, totalBytes: number) => void): Promise<UploadedImage> {

View File

@@ -33,8 +33,8 @@ type EffectStorage = {
setItem(name: string, value: string): unknown | Promise<unknown>;
};
const EFFECT_STORAGE_PREFIX = "infinite-canvas:generation-effect:";
const EFFECT_LOCK_PREFIX = "infinite-canvas:generation-effect-lock:";
const EFFECT_STORAGE_PREFIX = "yingce:generation-effect:";
const EFFECT_LOCK_PREFIX = "yingce:generation-effect-lock:";
const DEFAULT_LEASE_MS = 30_000;
const inProcessRecords = new Map<string, string>();
const inProcessLockTails = new Map<string, Promise<void>>();

View File

@@ -13,8 +13,8 @@ type ResourceCacheMeta = {
lastAccessedAt: number;
};
const blobStore = localforage.createInstance({ name: "infinite-canvas", storeName: "resource_blobs" });
const metaStore = localforage.createInstance({ name: "infinite-canvas", storeName: "resource_blob_meta" });
const blobStore = localforage.createInstance({ name: "yingce", storeName: "resource_blobs" });
const metaStore = localforage.createInstance({ name: "yingce", storeName: "resource_blob_meta" });
const objectUrls = new Map<string, string>();
const sessionBlobs = new Map<string, Blob>();
const inFlight = new Map<string, Promise<string>>();