From 02b9b299076334bf1e04feb4bfa5462ef03c5460 Mon Sep 17 00:00:00 2001 From: duanyongcheng <1171998654@qq.com> Date: Tue, 27 Jan 2026 20:34:48 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix(tools):=20=E4=BC=98=E5=8C=96=E5=A4=9A?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E5=86=85=E5=AE=B9=E6=8F=90=E5=8F=96=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=B7=A5=E5=85=B7=E5=88=86=E7=BB=84=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=20Optimize=20multimodal=20extraction=20and?= =?UTF-8?q?=20enhance=20tool=20group=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化了 AmpHeadersProcessor 中的多模态内容提取逻辑,使代码更简洁 Refactored multimodal content extraction logic in AmpHeadersProcessor for cleaner code 在添加或更新工具实例时自动补全缺失的工具分组,提升对旧版配置文件的兼容性 Automatically create missing tool groups when adding or updating instances to improve compatibility with legacy configuration files --- .../services/proxy/headers/amp_processor.rs | 10 ++-- src-tauri/src/services/tool/db.rs | 30 ++++++++++- src-tauri/src/services/tool/installer.rs | 31 +++++++---- .../src/services/tool/registry/instance.rs | 35 ++++++++---- src/components/ui/toast.tsx | 2 +- .../AddInstanceDialog/AddInstanceDialog.tsx | 9 ++-- .../hooks/useToolManagement.ts | 13 ++--- src/utils/error.ts | 53 +++++++++++++++++++ 8 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 src/utils/error.ts diff --git a/src-tauri/src/services/proxy/headers/amp_processor.rs b/src-tauri/src/services/proxy/headers/amp_processor.rs index 07293bf..fb36457 100644 --- a/src-tauri/src/services/proxy/headers/amp_processor.rs +++ b/src-tauri/src/services/proxy/headers/amp_processor.rs @@ -720,9 +720,9 @@ impl AmpHeadersProcessor { let c = m.get("content")?; if let Some(s) = c.as_str() { Some(s.to_string()) - } else if let Some(arr) = c.as_array() { + } else { // 多模态内容:提取 text 类型 - Some( + c.as_array().map(|arr| { arr.iter() .filter_map(|item| { if item.get("type")?.as_str()? == "text" { @@ -732,10 +732,8 @@ impl AmpHeadersProcessor { } }) .collect::>() - .join(""), - ) - } else { - None + .join("") + }) } }) .collect::>() diff --git a/src-tauri/src/services/tool/db.rs b/src-tauri/src/services/tool/db.rs index b907811..48df358 100644 --- a/src-tauri/src/services/tool/db.rs +++ b/src-tauri/src/services/tool/db.rs @@ -77,7 +77,20 @@ impl ToolInstanceDB { pub fn add_instance(&self, instance: &ToolInstance) -> Result<()> { let mut config = self.load_config()?; - // 查找对应的 ToolGroup + // 查找/创建对应的 ToolGroup(兼容旧 tools.json 缺少分组的情况) + if !config.tools.iter().any(|g| g.id == instance.base_id) { + tracing::warn!("tools.json 缺少工具分组 {},将自动补齐", instance.base_id); + config + .tools + .push(crate::services::tool::tools_config::ToolGroup { + id: instance.base_id.clone(), + name: instance.tool_name.clone(), + local_tools: vec![], + wsl_tools: vec![], + ssh_tools: vec![], + }); + } + let tool_group = config .tools .iter_mut() @@ -140,7 +153,20 @@ impl ToolInstanceDB { pub fn update_instance(&self, instance: &ToolInstance) -> Result<()> { let mut config = self.load_config()?; - // 查找对应的 ToolGroup + // 查找/创建对应的 ToolGroup(避免旧数据导致更新失败) + if !config.tools.iter().any(|g| g.id == instance.base_id) { + tracing::warn!("tools.json 缺少工具分组 {},将自动补齐", instance.base_id); + config + .tools + .push(crate::services::tool::tools_config::ToolGroup { + id: instance.base_id.clone(), + name: instance.tool_name.clone(), + local_tools: vec![], + wsl_tools: vec![], + ssh_tools: vec![], + }); + } + let tool_group = config .tools .iter_mut() diff --git a/src-tauri/src/services/tool/installer.rs b/src-tauri/src/services/tool/installer.rs index 938c7d5..35aef72 100644 --- a/src-tauri/src/services/tool/installer.rs +++ b/src-tauri/src/services/tool/installer.rs @@ -76,17 +76,29 @@ impl InstallerService { instance: &ToolInstance, force: bool, ) -> Result { - // 1. 检查是否有安装器路径和安装方法 - let installer_path = instance.installer_path.as_ref().ok_or_else(|| { - anyhow::anyhow!("该实例未配置安装器路径,无法执行快捷更新。请手动更新或重新添加实例。") - })?; - + // 1. 检查是否有安装方法 let install_method = instance .install_method .as_ref() .ok_or_else(|| anyhow::anyhow!("该实例未配置安装方法,无法执行快捷更新"))?; - // 2. 根据安装方法构建更新命令 + // 2. 不支持的安装方式:直接返回(避免误报“缺少安装器路径”) + match install_method { + InstallMethod::Official => { + anyhow::bail!("官方安装方式暂不支持快捷更新,请手动重新安装"); + } + InstallMethod::Other => { + anyhow::bail!("「其他」类型不支持 APP 内快捷更新,请手动更新"); + } + InstallMethod::Npm | InstallMethod::Brew => {} + } + + // 3. Npm/Brew:需要安装器路径 + let installer_path = instance.installer_path.as_ref().ok_or_else(|| { + anyhow::anyhow!("该实例未配置安装器路径,无法执行快捷更新。请手动更新或重新添加实例。") + })?; + + // 4. 根据安装方法构建更新命令 let tool_obj = Tool::by_id(&instance.base_id).ok_or_else(|| anyhow::anyhow!("未知工具"))?; let update_cmd = match install_method { @@ -102,11 +114,8 @@ impl InstallerService { let tool_id = &instance.base_id; format!("{} upgrade {}", installer_path, tool_id) } - InstallMethod::Official => { - anyhow::bail!("官方安装方式暂不支持快捷更新,请手动重新安装"); - } - InstallMethod::Other => { - anyhow::bail!("「其他」类型不支持 APP 内快捷更新,请手动更新"); + InstallMethod::Official | InstallMethod::Other => { + unreachable!("InstallMethod::Official/Other 已在前置 match 中提前返回") } }; diff --git a/src-tauri/src/services/tool/registry/instance.rs b/src-tauri/src/services/tool/registry/instance.rs index 7dc1318..0479536 100644 --- a/src-tauri/src/services/tool/registry/instance.rs +++ b/src-tauri/src/services/tool/registry/instance.rs @@ -113,7 +113,7 @@ impl ToolRegistry { /// - tool_id: 工具ID /// - path: 工具路径 /// - install_method: 安装方法 - /// - installer_path: 安装器路径(非 Other 类型时必需) + /// - installer_path: 安装器路径(Npm/Brew 用于快捷更新;Official/Other 可为空) /// /// # 返回 /// - Ok(ToolStatus): 工具状态 @@ -130,18 +130,31 @@ impl ToolRegistry { // 1. 验证工具路径 let version = self.validate_tool_path(path).await?; - // 2. 验证安装器路径(非 Other 类型时需要) - if install_method != InstallMethod::Other { - if let Some(ref installer) = installer_path { - let installer_buf = PathBuf::from(installer); - if !installer_buf.exists() { - anyhow::bail!("安装器路径不存在: {}", installer); + // 2. 验证安装器路径(仅 Npm/Brew 需要;Official/Other 允许为空) + match &install_method { + InstallMethod::Npm | InstallMethod::Brew => { + if let Some(ref installer) = installer_path { + let installer_buf = PathBuf::from(installer); + if !installer_buf.exists() { + anyhow::bail!("安装器路径不存在: {}", installer); + } + if !installer_buf.is_file() { + anyhow::bail!("安装器路径不是文件: {}", installer); + } + } else { + anyhow::bail!("Npm/Brew 类型必须提供安装器路径"); } - if !installer_buf.is_file() { - anyhow::bail!("安装器路径不是文件: {}", installer); + } + InstallMethod::Official | InstallMethod::Other => { + if let Some(ref installer) = installer_path { + let installer_buf = PathBuf::from(installer); + if !installer_buf.exists() { + anyhow::bail!("安装器路径不存在: {}", installer); + } + if !installer_buf.is_file() { + anyhow::bail!("安装器路径不是文件: {}", installer); + } } - } else { - anyhow::bail!("非「其他」类型必须提供安装器路径"); } } diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index da878c4..69e5483 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef< ; + +function isRecord(value: unknown): value is UnknownRecord { + return typeof value === 'object' && value !== null; +} + +function getStringField(obj: UnknownRecord, key: string): string | undefined { + const value = obj[key]; + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + +/** + * 将 unknown 错误转换为可读的字符串。 + * + * 说明: + * - Tauri commands 可能抛出结构化对象(如 AppError 序列化结果),直接 String(err) 会变成 "[object Object]". + */ +export function getErrorMessage(error: unknown): string { + if (error == null) return '未知错误'; + if (typeof error === 'string') return error; + if (error instanceof Error) return error.message || String(error); + if (typeof error === 'number' || typeof error === 'boolean' || typeof error === 'bigint') { + return String(error); + } + + if (isRecord(error)) { + const message = getStringField(error, 'message'); + if (message) return message; + + const type = getStringField(error, 'type'); + const field = getStringField(error, 'field'); + const reason = getStringField(error, 'reason'); + + if (type === 'ValidationError' && field && reason) { + return `验证失败:${field},原因:${reason}`; + } + + if (reason) return reason; + + const err = getStringField(error, 'error'); + if (err) return err; + + try { + return JSON.stringify(error); + } catch { + return String(error); + } + } + + return String(error); +} From 9414e0d842d32242a47be21a1dfebfe6b4420e41 Mon Sep 17 00:00:00 2001 From: duanyongcheng <1171998654@qq.com> Date: Tue, 27 Jan 2026 21:30:58 +0800 Subject: [PATCH 2/6] =?UTF-8?q?style(ui):=20=E8=B0=83=E6=95=B4=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=92=8C=E6=8F=90=E7=A4=BA=E7=BB=84=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=B1=82=E7=BA=A7=E4=B8=8E=E5=B8=83=E5=B1=80=20Adjust=20z-inde?= =?UTF-8?q?x=20and=20layout=20for=20AlertDialog=20and=20Toast=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 AlertDialog 的 z-index 提升至 200 以修复层级遮挡问题 Increase AlertDialog z-index to 200 to fix layering issues 优化 Toast 视口在不同屏幕尺寸下的布局和对齐方式 Optimize Toast viewport layout and alignment across different screen sizes --- src/components/ui/alert-dialog.tsx | 4 ++-- src/components/ui/toast.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 524f3dd..fad1e2a 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( Date: Thu, 29 Jan 2026 21:35:52 +0800 Subject: [PATCH 3/6] feat(layout): add responsive sidebar with auto-collapse and reduced width - Add auto-collapse when window width < 1024px (150ms debounce) - User manual preference takes priority over auto-collapse - Reduce sidebar width from w-64 (256px) to w-44 (176px) - Track user interaction in localStorage to preserve preferences --- src/components/layout/AppSidebar.tsx | 47 ++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index 66f8c7c..12df14a 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -19,7 +19,7 @@ import { import DuckLogo from '@/assets/duck-logo.png'; import { useToast } from '@/hooks/use-toast'; import { useTheme } from '@/hooks/useThemeHook'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { DropdownMenu, DropdownMenuContent, @@ -68,6 +68,9 @@ const navigationGroups = [ }, ]; +// 响应式折叠阈值 +const COLLAPSE_BREAKPOINT = 1024; + export function AppSidebar({ activeTab, onTabChange, @@ -77,15 +80,53 @@ export function AppSidebar({ const { toast } = useToast(); const { actualTheme, setTheme } = useTheme(); + // 用户是否手动操作过侧边栏(优先级高于自动折叠) + const [userHasInteracted, setUserHasInteracted] = useState(() => { + return localStorage.getItem('duckcoding-sidebar-user-interacted') === 'true'; + }); + const [isCollapsed, setIsCollapsed] = useState(() => { const stored = localStorage.getItem('duckcoding-sidebar-collapsed'); + // 如果用户没有手动操作过,根据窗口大小决定初始状态 + if (!localStorage.getItem('duckcoding-sidebar-user-interacted')) { + return typeof window !== 'undefined' && window.innerWidth < COLLAPSE_BREAKPOINT; + } return stored === 'true'; }); + // 持久化折叠状态 useEffect(() => { localStorage.setItem('duckcoding-sidebar-collapsed', String(isCollapsed)); }, [isCollapsed]); + // 响应式自动折叠(仅在用户未手动操作时生效) + useEffect(() => { + if (userHasInteracted) return; // 用户手动操作过,不自动折叠 + + let timeoutId: ReturnType; + + const handleResize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + const isSmallScreen = window.innerWidth < COLLAPSE_BREAKPOINT; + setIsCollapsed(isSmallScreen); + }, 150); + }; + + window.addEventListener('resize', handleResize); + return () => { + clearTimeout(timeoutId); + window.removeEventListener('resize', handleResize); + }; + }, [userHasInteracted]); + + // 手动切换折叠状态(标记用户已交互) + const handleToggleCollapse = useCallback(() => { + setIsCollapsed((prev) => !prev); + setUserHasInteracted(true); + localStorage.setItem('duckcoding-sidebar-user-interacted', 'true'); + }, []); + const handleTabChange = (tab: string) => { if (restrictNavigation) { if (allowedPage && tab !== allowedPage) { @@ -155,7 +196,7 @@ export function AppSidebar({ className={cn( 'flex flex-col border border-border/50 bg-card/50 backdrop-blur-xl shadow-sm transition-all duration-300 ease-in-out z-50', 'my-3 ml-3 rounded-2xl', - isCollapsed ? 'w-[68px]' : 'w-64', + isCollapsed ? 'w-[68px]' : 'w-44', )} > {/* Logo Header */} @@ -251,7 +292,7 @@ export function AppSidebar({ variant="ghost" size="icon" className="h-8 w-8 hover:bg-background/80 hover:text-primary transition-colors" - onClick={() => setIsCollapsed(!isCollapsed)} + onClick={handleToggleCollapse} > {isCollapsed ? ( From 17b7805536a90c5a8bd9b73cfe3290274c374aaf Mon Sep 17 00:00:00 2001 From: duanyongcheng <1171998654@qq.com> Date: Thu, 29 Jan 2026 21:36:13 +0800 Subject: [PATCH 4/6] style(layout): reduce spacing in MainLayout and PageContainer for compact display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MainLayout: p-6 → p-4 (reduce padding by 33%) - PageContainer: space-y-6 → space-y-4, text-2xl → text-xl, pb-8 → pb-6 - Optimize for 1024x768 resolution --- src/components/layout/MainLayout.tsx | 2 +- src/components/layout/PageContainer.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index d55e532..09fc65c 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -25,7 +25,7 @@ export function MainLayout({ children }: MainLayoutProps) {
{/* Scrollable Content */} -
+
{children}
diff --git a/src/components/layout/PageContainer.tsx b/src/components/layout/PageContainer.tsx index c8189cc..ec006f5 100644 --- a/src/components/layout/PageContainer.tsx +++ b/src/components/layout/PageContainer.tsx @@ -23,12 +23,12 @@ export function PageContainer({ actions, }: PageContainerProps) { return ( -
+
{/* Optional Standard Header Section */} {(title || header || actions) && ( -
-
- {title &&

{title}

} +
+
+ {title &&

{title}

} {description &&

{description}

} {header}
@@ -37,7 +37,7 @@ export function PageContainer({ )} {/* Main Content */} -
{children}
+
{children}
); } From d996c944a7fcff9751c25f5fae147df8f7554b01 Mon Sep 17 00:00:00 2001 From: duanyongcheng <1171998654@qq.com> Date: Thu, 29 Jan 2026 21:36:29 +0800 Subject: [PATCH 5/6] style(pages): optimize grid breakpoints and spacing for 1024x768 display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce tab heights: h-11 → h-9 - Reduce tab margins: mb-6 → mb-4 - Reduce grid gaps: gap-6 → gap-4 - Reduce section spacing: space-y-8 → space-y-6, space-y-6 → space-y-4 - Optimize for compact layout on smaller screens --- src/pages/DashboardPage/index.tsx | 8 ++++---- src/pages/ProfileManagementPage/index.tsx | 6 +++--- src/pages/SettingsPage/index.tsx | 2 +- src/pages/TokenStatisticsPage/components/Dashboard.tsx | 4 ++-- src/pages/ToolManagementPage/index.tsx | 2 +- src/pages/TransparentProxyPage/index.tsx | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/DashboardPage/index.tsx b/src/pages/DashboardPage/index.tsx index 031ef2b..ed723aa 100644 --- a/src/pages/DashboardPage/index.tsx +++ b/src/pages/DashboardPage/index.tsx @@ -338,11 +338,11 @@ export function DashboardPage() {
-
+
{/* 工具状态 */} -
+

工具管理

-
+
{displayTools.map((tool) => ( {/* 供应商与用量 */} -
+

供应商与用量统计