1 parent
72420895b2
commit
d3b044e009
9 files changed
+35
-9
No files matched your search
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- 修复 HTTP 内网 IP 访问时 `crypto.randomUUID` 不可用导致创造页卡死,并统一画布、Agent 与生成请求的客户端 ID 生成方式。
|
||||||
- 画布绘图节点支持 tldraw 与 Excalidraw 双引擎,管理员可配置新建默认工具,并保持历史绘图和导入导出兼容。
|
- 画布绘图节点支持 tldraw 与 Excalidraw 双引擎,管理员可配置新建默认工具,并保持历史绘图和导入导出兼容。
|
||||||
|
|
||||||
## v1.0.32
|
## v1.0.32
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, MessageSquare
|
|||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
import { useThemeStore } from "@/stores/use-theme-store";
|
||||||
import { useUserStore } from "@/stores/use-user-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";
|
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 connectedRef = useRef(false);
|
||||||
const errorLoggedRef = useRef(false);
|
const errorLoggedRef = useRef(false);
|
||||||
const attachmentUrlsRef = useRef(new Set<string>());
|
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 endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]);
|
||||||
const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken");
|
const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken");
|
||||||
const loadThreads = useCallback(async () => {
|
const loadThreads = useCallback(async () => {
|
||||||
@@ -1017,7 +1018,7 @@ function formatThreadTime(value?: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createId() {
|
function createId() {
|
||||||
return typeof crypto === "undefined" ? `${Date.now()}-${Math.random()}` : crypto.randomUUID();
|
return createClientId();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
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 { FileId } from "@excalidraw/excalidraw/element/types";
|
||||||
import type { BinaryFileData, BinaryFiles, DataURL } from "@excalidraw/excalidraw/types";
|
import type { BinaryFileData, BinaryFiles, DataURL } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
|
|
||||||
const INITIAL_DRAWING_SHAPE_MAX_DIMENSION = 1200;
|
const INITIAL_DRAWING_SHAPE_MAX_DIMENSION = 1200;
|
||||||
|
|
||||||
type DrawingImageSource = {
|
type DrawingImageSource = {
|
||||||
@@ -14,7 +16,7 @@ type DrawingImageSource = {
|
|||||||
|
|
||||||
export function createExcalidrawDrawingFromImage(source: DrawingImageSource) {
|
export function createExcalidrawDrawingFromImage(source: DrawingImageSource) {
|
||||||
const now = Date.now();
|
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 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 width = Math.max(1, Math.round(source.width * scale));
|
||||||
const height = Math.max(1, Math.round(source.height * scale));
|
const height = Math.max(1, Math.round(source.height * scale));
|
||||||
|
|||||||
@@ -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 { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||||
import { ModelPicker } from "@/components/model-picker";
|
import { ModelPicker } from "@/components/model-picker";
|
||||||
import { canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
|
import { canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
import { generationErrorMessage } from "@/lib/generation-error";
|
import { generationErrorMessage } from "@/lib/generation-error";
|
||||||
import { VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
import { VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
||||||
import { parseBackendGenerationResult, runBackendGenerationTask, runBackendGenerationTaskBatch } from "@/services/api/generation-task";
|
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 });
|
const conversationTimeFormatter = new Intl.DateTimeFormat("zh-CN", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
||||||
|
|
||||||
function newConversation(): CreationConversation {
|
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 {
|
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() {
|
export default function CreatePage() {
|
||||||
@@ -195,7 +196,7 @@ export default function CreatePage() {
|
|||||||
if (!next.length) return;
|
if (!next.length) return;
|
||||||
void Promise.all(next.slice(0, 6).map(async (file) => {
|
void Promise.all(next.slice(0, 6).map(async (file) => {
|
||||||
const dataUrl = await readFileAsDataUrl(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("参考图读取失败,请重试"));
|
})).then((items) => setAttachments((current) => [...current, ...items].slice(0, 6))).catch(() => toast.error("参考图读取失败,请重试"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { audioMimeType, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
|
import { audioMimeType, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
import { channelRequest } from "@/services/api/custom-channel-relay";
|
import { channelRequest } from "@/services/api/custom-channel-relay";
|
||||||
import { uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
import { uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||||
import { buildApiUrl, isSystemProxyBaseUrl, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
import { buildApiUrl, isSystemProxyBaseUrl, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||||
@@ -15,7 +16,7 @@ function aiHeaders(config: AiConfig) {
|
|||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
"Content-Type": "application/json",
|
"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 { nanoid } from "nanoid";
|
||||||
import { dataUrlToFile } from "@/lib/image-utils";
|
import { dataUrlToFile } from "@/lib/image-utils";
|
||||||
import { buildImageReferencePromptText } from "@/lib/image-reference-prompt";
|
import { buildImageReferencePromptText } from "@/lib/image-reference-prompt";
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
import { channelRequest } from "@/services/api/custom-channel-relay";
|
import { channelRequest } from "@/services/api/custom-channel-relay";
|
||||||
import { imageToDataUrl } from "@/services/image-storage";
|
import { imageToDataUrl } from "@/services/image-storage";
|
||||||
import type { ReferenceImage } from "@/types/image";
|
import type { ReferenceImage } from "@/types/image";
|
||||||
@@ -262,7 +263,7 @@ function aiHeaders(config: AiConfig, contentType?: string) {
|
|||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
...(contentType ? { "Content-Type": contentType } : {}),
|
...(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 axios from "axios";
|
||||||
|
|
||||||
|
import { createClientId } from "@/lib/client-id";
|
||||||
import { dataUrlToFile } from "@/lib/image-utils";
|
import { dataUrlToFile } from "@/lib/image-utils";
|
||||||
import { getMediaBlob, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
import { getMediaBlob, uploadMediaFile, type UploadedFile } from "@/services/file-storage";
|
||||||
import { getResourceOSSUrl } from "@/services/api/resources";
|
import { getResourceOSSUrl } from "@/services/api/resources";
|
||||||
@@ -37,7 +38,7 @@ function aiHeaders(config: AiConfig, contentType?: string) {
|
|||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
...(contentType ? { "Content-Type": contentType } : {}),
|
...(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() } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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