From c1dee85366a9c8bbe17c2c47d600e1bedf9cd90e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 5 Mar 2026 10:13:19 +0800 Subject: [PATCH] fix(canvas): refine toolbar labels/style and harden Tripo API key handling --- components/canvas/nodes/ImageNode.tsx | 54 +++++++++++++-------------- lib/tripo/client.ts | 32 +++++++++++++++- lib/tripo/server.ts | 27 +++++++++++--- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 4421fcd43..e451282bb 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { createPortal } from "react-dom"; -import { ImagePlus, Loader2, AlertCircle, MoreVertical, MoreHorizontal, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react"; +import { ImagePlus, Loader2, AlertCircle, MoreVertical, MoreHorizontal, Scissors, Eraser, Sparkles, Expand, Maximize2, Box, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -816,12 +816,13 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const getToolbarPosition = () => { if (containerRef.current && typeof window !== "undefined") { const rect = containerRef.current.getBoundingClientRect(); - const toolbarHeight = 40; - const titleHeight = 28; - const gap = 10; - let top = rect.top - toolbarHeight - titleHeight - gap; + const toolbarHeight = 48; + const titleSafeHeight = 34; + const gap = 12; + let top = rect.top - toolbarHeight - titleSafeHeight - gap; if (top < 12) { - top = rect.top + 8; + const maxTop = Math.max(12, window.innerHeight - toolbarHeight - 12); + top = Math.min(rect.bottom + 10, maxTop); } const minLeft = 80; @@ -1940,7 +1941,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 调尺 handleAction('angle')}> - + 视角 handleAction('prompt-regenerate')}> @@ -2058,17 +2059,18 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { style={{ top: getToolbarPosition().top, left: getToolbarPosition().left, - transform: 'translateX(-50%)', + transform: 'translateX(-50%) scale(0.8)', + transformOrigin: 'top center', pointerEvents: 'auto', }} onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} > -
+
- 快捷编辑 - Tab + 快捷编辑 + Tab
@@ -2080,7 +2082,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { onClick={() => handleAction('enhance')} > HD - 放大 + 高清增强 - -
@@ -2156,7 +2148,11 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { - + handleAction('extend')}> 扩图 diff --git a/lib/tripo/client.ts b/lib/tripo/client.ts index 547d2acb2..7a1531131 100644 --- a/lib/tripo/client.ts +++ b/lib/tripo/client.ts @@ -42,6 +42,20 @@ function normalizeBaseUrl(baseUrl?: string) { return resolved.replace(/\/+$/, ""); } +function sanitizeHeaderText(value: string): string { + return String(value || "") + .replace(/[\u2028\u2029]/g, "") + .replace(/[\u0000-\u001F\u007F]/g, "") + .trim(); +} + +function hasNonByteStringChar(value: string): boolean { + for (let i = 0; i < value.length; i += 1) { + if (value.charCodeAt(i) > 255) return true; + } + return false; +} + async function parseResponseBody(response: Response): Promise { const contentType = response.headers.get("content-type") || ""; if (contentType.includes("application/json")) { @@ -59,7 +73,23 @@ export async function tripoRequest( const path = options.path.startsWith("/") ? options.path : `/${options.path}`; const url = `${normalizeBaseUrl(config.baseUrl)}${path}`; const headers = new Headers(options.headers || {}); - headers.set("Authorization", `Bearer ${config.apiKey}`); + + const normalizedApiKey = sanitizeHeaderText(String(config.apiKey || "")).replace(/\s+/g, ""); + if (!normalizedApiKey) { + throw new TripoApiError({ + message: "Tripo3D API Key 为空,请在本地AI设置中重新保存", + httpStatus: 400, + }); + } + if (hasNonByteStringChar(normalizedApiKey)) { + throw new TripoApiError({ + message: + "Tripo3D API Key 含非法字符(可能是复制时带入的隐藏换行符),请在本地AI设置中重新粘贴保存", + httpStatus: 400, + }); + } + + headers.set("Authorization", `Bearer ${normalizedApiKey}`); headers.set("Accept", "application/json"); let body: BodyInit | undefined; diff --git a/lib/tripo/server.ts b/lib/tripo/server.ts index f1973ad1a..0c2293401 100644 --- a/lib/tripo/server.ts +++ b/lib/tripo/server.ts @@ -5,27 +5,42 @@ import type { TripoProviderConfig } from "./types"; const DEFAULT_TRIPO_BASE_URL = "https://api.tripo3d.ai/v2/openapi"; +function sanitizeTripoApiKey(value: string): string { + return String(value || "") + .replace(/[\u2028\u2029]/g, "") + .replace(/[\u0000-\u001F\u007F]/g, "") + .replace(/\s+/g, "") + .trim(); +} + +function sanitizeTripoBaseUrl(value: string): string { + return String(value || "") + .replace(/[\u2028\u2029]/g, "") + .replace(/[\u0000-\u001F\u007F]/g, "") + .trim(); +} + export async function getTripoProviderConfig(): Promise { const providers = await getEnabledAiProviders(); const tripoProvider = providers.find((item) => item.type === "tripo-3d"); - if (tripoProvider?.apiKey?.trim()) { + const providerApiKey = sanitizeTripoApiKey(String(tripoProvider?.apiKey || "")); + if (tripoProvider && providerApiKey) { return { - apiKey: tripoProvider.apiKey.trim(), - baseUrl: String(tripoProvider.baseUrl || "").trim() || DEFAULT_TRIPO_BASE_URL, + apiKey: providerApiKey, + baseUrl: sanitizeTripoBaseUrl(String(tripoProvider?.baseUrl || "")) || DEFAULT_TRIPO_BASE_URL, providerId: tripoProvider.id, providerName: tripoProvider.name, }; } - const envApiKey = String(process.env.TRIPO_API_KEY || "").trim(); + const envApiKey = sanitizeTripoApiKey(String(process.env.TRIPO_API_KEY || "")); if (!envApiKey) { throw new Error("未配置 Tripo3D API,请在系统设置-本地AI新增 Tripo3D 提供商"); } return { apiKey: envApiKey, - baseUrl: String(process.env.TRIPO_BASE_URL || "").trim() || DEFAULT_TRIPO_BASE_URL, + baseUrl: sanitizeTripoBaseUrl(String(process.env.TRIPO_BASE_URL || "")) || DEFAULT_TRIPO_BASE_URL, }; } -