diff --git a/.gitignore b/.gitignore index cfb6b98..29a28e7 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ mirror-server/ /.claude/ /src-tauri/NUL /docs +.sisyphus 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/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 ? ( 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}
); } 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) => (
-
+
{/* 工具状态 */} -
+

工具管理

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

供应商与用量统计