From d3b044e00946be8ce4f46ae8705fc9ad4b2aa312 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Mon, 3 Aug 2026 15:06:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(*):=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=A0=87?= =?UTF-8?q?=E8=AF=86=20-=20=E5=85=BC=E5=AE=B9=20HTTP=20=E5=86=85=E7=BD=91?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=88=9B=E9=80=A0?= =?UTF-8?q?=E9=A1=B5=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #86 --- CHANGELOG.md | 1 + .../components/canvas/canvas-local-agent-panel.tsx | 5 +++-- .../lib/canvas/canvas-drawing-excalidraw-document.ts | 4 +++- web/src/lib/client-id.ts | 6 ++++++ web/src/pages/create/index.tsx | 7 ++++--- web/src/services/api/audio.ts | 3 ++- web/src/services/api/image.ts | 3 ++- web/src/services/api/video.ts | 3 ++- web/test/client-id.test.ts | 12 ++++++++++++ 9 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 web/src/lib/client-id.ts create mode 100644 web/test/client-id.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe8d523d..18c1d5ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- 修复 HTTP 内网 IP 访问时 `crypto.randomUUID` 不可用导致创造页卡死,并统一画布、Agent 与生成请求的客户端 ID 生成方式。 - 画布绘图节点支持 tldraw 与 Excalidraw 双引擎,管理员可配置新建默认工具,并保持历史绘图和导入导出兼容。 ## v1.0.32 diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx index fa1b931c4..d46ac5130 100644 --- a/web/src/components/canvas/canvas-local-agent-panel.tsx +++ b/web/src/components/canvas/canvas-local-agent-panel.tsx @@ -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()); - 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) { diff --git a/web/src/lib/canvas/canvas-drawing-excalidraw-document.ts b/web/src/lib/canvas/canvas-drawing-excalidraw-document.ts index b1d1ede87..f7f09b1e6 100644 --- a/web/src/lib/canvas/canvas-drawing-excalidraw-document.ts +++ b/web/src/lib/canvas/canvas-drawing-excalidraw-document.ts @@ -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)); diff --git a/web/src/lib/client-id.ts b/web/src/lib/client-id.ts new file mode 100644 index 000000000..b19827352 --- /dev/null +++ b/web/src/lib/client-id.ts @@ -0,0 +1,6 @@ +import { nanoid } from "nanoid"; + +export function createClientId() { + // randomUUID 在 HTTP 内网地址下不可用;nanoid 依赖仍可用的 getRandomValues。 + return nanoid(); +} diff --git a/web/src/pages/create/index.tsx b/web/src/pages/create/index.tsx index ae35667b5..38087eacd 100644 --- a/web/src/pages/create/index.tsx +++ b/web/src/pages/create/index.tsx @@ -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 { - 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("参考图读取失败,请重试")); }; diff --git a/web/src/services/api/audio.ts b/web/src/services/api/audio.ts index a51e19f89..317c6259e 100644 --- a/web/src/services/api/audio.ts +++ b/web/src/services/api/audio.ts @@ -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() } : {}), }; } diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts index 3861ed849..f2773d890 100644 --- a/web/src/services/api/image.ts +++ b/web/src/services/api/image.ts @@ -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() } : {}), }; } diff --git a/web/src/services/api/video.ts b/web/src/services/api/video.ts index 7f2d621f6..f5fd312b6 100644 --- a/web/src/services/api/video.ts +++ b/web/src/services/api/video.ts @@ -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() } : {}), }; } diff --git a/web/test/client-id.test.ts b/web/test/client-id.test.ts new file mode 100644 index 000000000..bb699d37c --- /dev/null +++ b/web/test/client-id.test.ts @@ -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}$/)); + }); +});