@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- 修复 HTTP 内网 IP 访问时 `crypto.randomUUID` 不可用导致创造页卡死,并统一画布、Agent 与生成请求的客户端 ID 生成方式。
|
||||
- 画布绘图节点支持 tldraw 与 Excalidraw 双引擎,管理员可配置新建默认工具,并保持历史绘图和导入导出兼容。
|
||||
|
||||
## v1.0.32
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { useCanvasAgentStore, type AgentAttachment, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "@/stores/canvas/use-canvas-agent-store";
|
||||
@@ -57,7 +58,7 @@ export const CanvasLocalAgentPanel = memo(function CanvasLocalAgentPanel({ snaps
|
||||
const connectedRef = useRef(false);
|
||||
const errorLoggedRef = useRef(false);
|
||||
const attachmentUrlsRef = useRef(new Set<string>());
|
||||
const clientIdRef = useRef(typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID());
|
||||
const clientIdRef = useRef(createClientId());
|
||||
const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]);
|
||||
const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken");
|
||||
const loadThreads = useCallback(async () => {
|
||||
@@ -1017,7 +1018,7 @@ function formatThreadTime(value?: number) {
|
||||
}
|
||||
|
||||
function createId() {
|
||||
return typeof crypto === "undefined" ? `${Date.now()}-${Math.random()}` : crypto.randomUUID();
|
||||
return createClientId();
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
import type { FileId } from "@excalidraw/excalidraw/element/types";
|
||||
import type { BinaryFileData, BinaryFiles, DataURL } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
|
||||
const INITIAL_DRAWING_SHAPE_MAX_DIMENSION = 1200;
|
||||
|
||||
type DrawingImageSource = {
|
||||
@@ -14,7 +16,7 @@ type DrawingImageSource = {
|
||||
|
||||
export function createExcalidrawDrawingFromImage(source: DrawingImageSource) {
|
||||
const now = Date.now();
|
||||
const fileId = crypto.randomUUID() as FileId;
|
||||
const fileId = createClientId() as FileId;
|
||||
const scale = Math.min(1, INITIAL_DRAWING_SHAPE_MAX_DIMENSION / Math.max(source.width, source.height));
|
||||
const width = Math.max(1, Math.round(source.width * scale));
|
||||
const height = Math.max(1, Math.round(source.height * scale));
|
||||
|
||||
6
web/src/lib/client-id.ts
Normal file
6
web/src/lib/client-id.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export function createClientId() {
|
||||
// randomUUID 在 HTTP 内网地址下不可用;nanoid 依赖仍可用的 getRandomValues。
|
||||
return nanoid();
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { Link } from "react-router";
|
||||
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
import { canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
import { generationErrorMessage } from "@/lib/generation-error";
|
||||
import { VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
||||
import { parseBackendGenerationResult, runBackendGenerationTask, runBackendGenerationTaskBatch } from "@/services/api/generation-task";
|
||||
@@ -60,11 +61,11 @@ const countOptions = ["1", "2", "3", "4"];
|
||||
const conversationTimeFormatter = new Intl.DateTimeFormat("zh-CN", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
|
||||
function newConversation(): CreationConversation {
|
||||
return { id: crypto.randomUUID(), title: "新创作", updatedAt: new Date().toISOString(), messages: [] };
|
||||
return { id: createClientId(), title: "新创作", updatedAt: new Date().toISOString(), messages: [] };
|
||||
}
|
||||
|
||||
function newMessage(role: CreationMessage["role"], content: string, extra: Partial<CreationMessage> = {}): CreationMessage {
|
||||
return { id: crypto.randomUUID(), role, content, createdAt: new Date().toISOString(), ...extra };
|
||||
return { id: createClientId(), role, content, createdAt: new Date().toISOString(), ...extra };
|
||||
}
|
||||
|
||||
export default function CreatePage() {
|
||||
@@ -195,7 +196,7 @@ export default function CreatePage() {
|
||||
if (!next.length) return;
|
||||
void Promise.all(next.slice(0, 6).map(async (file) => {
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
return { id: crypto.randomUUID(), name: file.name, type: file.type, dataUrl, previewUrl: dataUrl } satisfies CreationAttachment;
|
||||
return { id: createClientId(), name: file.name, type: file.type, dataUrl, previewUrl: dataUrl } satisfies CreationAttachment;
|
||||
})).then((items) => setAttachments((current) => [...current, ...items].slice(0, 6))).catch(() => toast.error("参考图读取失败,请重试"));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { audioMimeType, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
import { channelRequest } from "@/services/api/custom-channel-relay";
|
||||
import { uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||
import { buildApiUrl, isSystemProxyBaseUrl, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
@@ -15,7 +16,7 @@ function aiHeaders(config: AiConfig) {
|
||||
return {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "audio", "X-Idempotency-Key": crypto.randomUUID() } : {}),
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "audio", "X-Idempotency-Key": createClientId() } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buildApiUrl, isSystemProxyBaseUrl, resolveBackendApiUrl, resolveModelRe
|
||||
import { nanoid } from "nanoid";
|
||||
import { dataUrlToFile } from "@/lib/image-utils";
|
||||
import { buildImageReferencePromptText } from "@/lib/image-reference-prompt";
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
import { channelRequest } from "@/services/api/custom-channel-relay";
|
||||
import { imageToDataUrl } from "@/services/image-storage";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
@@ -262,7 +263,7 @@ function aiHeaders(config: AiConfig, contentType?: string) {
|
||||
return {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "image", "X-Idempotency-Key": crypto.randomUUID() } : {}),
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "image", "X-Idempotency-Key": createClientId() } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { createClientId } from "@/lib/client-id";
|
||||
import { dataUrlToFile } from "@/lib/image-utils";
|
||||
import { getMediaBlob, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||
import { getResourceOSSUrl } from "@/services/api/resources";
|
||||
@@ -37,7 +38,7 @@ function aiHeaders(config: AiConfig, contentType?: string) {
|
||||
return {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "video", "X-Idempotency-Key": crypto.randomUUID() } : {}),
|
||||
...(isSystemProxyBaseUrl(config.baseUrl) ? { "X-Canvas-Scene": "video", "X-Idempotency-Key": createClientId() } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
12
web/test/client-id.test.ts
Normal file
12
web/test/client-id.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { createClientId } from "../src/lib/client-id";
|
||||
|
||||
describe("createClientId", () => {
|
||||
test("生成不依赖 randomUUID 的唯一客户端标识", () => {
|
||||
const ids = Array.from({ length: 100 }, () => createClientId());
|
||||
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
ids.forEach((id) => expect(id).toMatch(/^[A-Za-z0-9_-]{21}$/));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user