Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ mirror-server/
/.claude/
/src-tauri/NUL
/docs
.sisyphus
10 changes: 4 additions & 6 deletions src-tauri/src/services/proxy/headers/amp_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -732,10 +732,8 @@ impl AmpHeadersProcessor {
}
})
.collect::<Vec<_>>()
.join(""),
)
} else {
None
.join("")
})
}
})
.collect::<Vec<_>>()
Expand Down
30 changes: 28 additions & 2 deletions src-tauri/src/services/tool/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
31 changes: 20 additions & 11 deletions src-tauri/src/services/tool/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,29 @@ impl InstallerService {
instance: &ToolInstance,
force: bool,
) -> Result<UpdateResult> {
// 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 {
Expand All @@ -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 中提前返回")
}
};

Expand Down
35 changes: 24 additions & 11 deletions src-tauri/src/services/tool/registry/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ impl ToolRegistry {
/// - tool_id: 工具ID
/// - path: 工具路径
/// - install_method: 安装方法
/// - installer_path: 安装器路径(Other 类型时必需
/// - installer_path: 安装器路径(Npm/Brew 用于快捷更新;Official/Other 可为空
///
/// # 返回
/// - Ok(ToolStatus): 工具状态
Expand All @@ -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!("非「其他」类型必须提供安装器路径");
}
}

Expand Down
47 changes: 44 additions & 3 deletions src/components/layout/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -68,6 +68,9 @@ const navigationGroups = [
},
];

// 响应式折叠阈值
const COLLAPSE_BREAKPOINT = 1024;

export function AppSidebar({
activeTab,
onTabChange,
Expand All @@ -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<typeof setTimeout>;

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) {
Expand Down Expand Up @@ -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 */}
Expand Down Expand Up @@ -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 ? (
<ChevronsRight className="h-4 w-4" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function MainLayout({ children }: MainLayoutProps) {
<div className="absolute inset-0 bg-gradient-to-br from-slate-50/50 to-slate-100/50 dark:from-slate-900/50 dark:to-slate-950/50 -z-10" />

{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-6 custom-scrollbar">
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 custom-scrollbar">
<div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500">
{children}
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/components/layout/PageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export function PageContainer({
actions,
}: PageContainerProps) {
return (
<div className={cn('space-y-6 pb-8', className)}>
<div className={cn('space-y-4 pb-6', className)}>
{/* Optional Standard Header Section */}
{(title || header || actions) && (
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="space-y-1.5">
{title && <h1 className="text-2xl font-bold tracking-tight">{title}</h1>}
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
{title && <h1 className="text-xl font-bold tracking-tight">{title}</h1>}
{description && <p className="text-sm text-muted-foreground">{description}</p>}
{header}
</div>
Expand All @@ -37,7 +37,7 @@ export function PageContainer({
)}

{/* Main Content */}
<div className="space-y-6">{children}</div>
<div className="space-y-4">{children}</div>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-[200] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
Expand All @@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-[50%] top-[50%] z-[200] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
Expand Down
8 changes: 4 additions & 4 deletions src/pages/DashboardPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,11 @@ export function DashboardPage() {
</Card>
</div>

<div className="space-y-8">
<div className="space-y-6">
{/* 工具状态 */}
<div className="space-y-4">
<div className="space-y-3">
<h3 className="text-lg font-semibold tracking-tight">工具管理</h3>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{displayTools.map((tool) => (
<DashboardToolCard
key={tool.id}
Expand All @@ -365,7 +365,7 @@ export function DashboardPage() {
</div>

{/* 供应商与用量 */}
<div className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold tracking-tight">供应商与用量统计</h3>
<Button
Expand Down
6 changes: 3 additions & 3 deletions src/pages/ProfileManagementPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export default function ProfileManagementPage() {
<>
{/* 工具 Tab 切换 */}
<Tabs value={selectedTab} onValueChange={(v) => setSelectedTab(v as ToolId)}>
<TabsList className="grid w-full grid-cols-4 mb-6 h-11 p-1 bg-muted/50 rounded-lg">
<TabsList className="grid w-full grid-cols-4 mb-4 h-9 p-1 bg-muted/50 rounded-lg">
{profileGroups.map((group) => (
<TabsTrigger
key={group.tool_id}
Expand All @@ -174,14 +174,14 @@ export default function ProfileManagementPage() {

{/* 每个工具的 Profile 列表 */}
{profileGroups.map((group) => (
<TabsContent key={group.tool_id} value={group.tool_id} className="space-y-6 mt-0">
<TabsContent key={group.tool_id} value={group.tool_id} className="space-y-4 mt-0">
{/* 当前生效配置卡片 */}
<ActiveProfileCard
group={group}
proxyRunning={allProxyStatus[group.tool_id]?.running || false}
/>

<div className="space-y-4">
<div className="space-y-3">
{/* 创建按钮 */}
<div className="flex items-center justify-between">
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/SettingsPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function SettingsPage() {
</div>
)}

<Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-6">
<Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
<TabsList>
<TabsTrigger value="basic" disabled={!!restrictToTab && restrictToTab !== 'basic'}>
系统设置
Expand Down
4 changes: 2 additions & 2 deletions src/pages/TokenStatisticsPage/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export interface DashboardProps {
export const Dashboard: React.FC<DashboardProps> = ({ summary, loading = false }) => {
if (loading) {
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-32 animate-pulse rounded-lg bg-gray-200 dark:bg-gray-700" />
))}
Expand All @@ -135,7 +135,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ summary, loading = false }
summary.total_requests > 0 ? (summary.failed_requests / summary.total_requests) * 100 : 0;

return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* 总成本 */}
<MetricCard
title="总成本"
Expand Down
Loading