release: v0.55.0 - 代码重构与 ESLint 修复

- 更新版本号到 0.55.0
- 修复 ESLint 警告:将 CREATION_MODE_CONFIG 移至单独的 constants.tsx
- 修复 ESLint 警告:将 CreationMode 类型移至 types.ts
- 修复 ProviderPoolPage 未使用变量警告
- 修复 Rust 测试中缺少的 Config 字段

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-02-03 12:42:19 +08:00
co-authored by Claude Opus 4.5
parent 2ba5eaa4c8
commit 08ad7a332e
15 changed files with 558 additions and 112 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.54.0",
"version": "0.55.0",
"type": "module",
"repository": {
"type": "git",
+3 -3
View File
@@ -6553,7 +6553,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.54.0"
version = "0.55.0"
dependencies = [
"anyhow",
"arboard",
@@ -6635,7 +6635,7 @@ dependencies = [
[[package]]
name = "proxycast-core"
version = "0.54.0"
version = "0.55.0"
dependencies = [
"chrono",
"dirs 5.0.1",
@@ -6651,7 +6651,7 @@ dependencies = [
[[package]]
name = "proxycast-infra"
version = "0.54.0"
version = "0.55.0"
dependencies = [
"chrono",
"dashmap 5.5.3",
+2 -2
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.54.0"
version = "0.55.0"
edition = "2021"
authors = ["you"]
repository = "https://github.com/aiclientproxy/proxycast"
@@ -164,7 +164,7 @@ version = "2.4"
[package]
name = "proxycast"
version = "0.54.0"
version = "0.55.0"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
+7
View File
@@ -2,6 +2,7 @@
//!
//! 使用 proptest 进行属性测试
use crate::config::types::{ContentCreatorConfig, NavigationConfig};
use crate::config::{
collapse_tilde, contains_tilde, expand_tilde, Config, ConfigManager, CustomProviderConfig,
HotReloadManager, InjectionSettings, LoggingConfig, ProviderConfig, ProvidersConfig,
@@ -189,6 +190,8 @@ fn arb_config() -> impl Strategy<Value = Config> {
agent: crate::config::NativeAgentConfig::default(),
language: "zh".to_string(),
experimental: crate::config::ExperimentalFeatures::default(),
content_creator: ContentCreatorConfig::default(),
navigation: NavigationConfig::default(),
})
}
@@ -427,6 +430,8 @@ fn arb_valid_config() -> impl Strategy<Value = Config> {
agent: crate::config::NativeAgentConfig::default(),
language: "zh".to_string(),
experimental: crate::config::ExperimentalFeatures::default(),
content_creator: ContentCreatorConfig::default(),
navigation: NavigationConfig::default(),
})
}
@@ -476,6 +481,8 @@ fn arb_invalid_config() -> impl Strategy<Value = Config> {
agent: crate::config::NativeAgentConfig::default(),
language: "zh".to_string(),
experimental: crate::config::ExperimentalFeatures::default(),
content_creator: ContentCreatorConfig::default(),
navigation: NavigationConfig::default(),
};
// 根据类型使配置无效
match invalid_type {
+69
View File
@@ -425,6 +425,12 @@ pub struct Config {
/// 实验室功能配置
#[serde(default)]
pub experimental: ExperimentalFeatures,
/// 内容创作配置
#[serde(default)]
pub content_creator: ContentCreatorConfig,
/// 导航栏配置
#[serde(default)]
pub navigation: NavigationConfig,
}
// ============ Native Agent 配置类型 ============
@@ -487,6 +493,67 @@ impl Default for NativeAgentConfig {
}
}
// ============ 内容创作配置类型 ============
/// 内容创作主题配置
///
/// 配置内容创作模式中显示的主题标签
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ContentCreatorConfig {
/// 启用的主题列表
#[serde(default = "default_enabled_themes")]
pub enabled_themes: Vec<String>,
}
fn default_enabled_themes() -> Vec<String> {
vec![
"general".to_string(),
"social-media".to_string(),
"poster".to_string(),
"music".to_string(),
"video".to_string(),
"novel".to_string(),
]
}
impl Default for ContentCreatorConfig {
fn default() -> Self {
Self {
enabled_themes: default_enabled_themes(),
}
}
}
// ============ 导航栏配置类型 ============
/// 导航栏模块配置
///
/// 配置左侧导航栏中显示的功能模块
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NavigationConfig {
/// 启用的导航模块列表
#[serde(default = "default_enabled_nav_items")]
pub enabled_items: Vec<String>,
}
fn default_enabled_nav_items() -> Vec<String> {
vec![
"agent".to_string(),
"projects".to_string(),
"image-gen".to_string(),
"api-server".to_string(),
"provider-pool".to_string(),
]
}
impl Default for NavigationConfig {
fn default() -> Self {
Self {
enabled_items: default_enabled_nav_items(),
}
}
}
// ============ 实验室功能配置类型 ============
/// 截图对话功能配置
@@ -1653,6 +1720,8 @@ impl Default for Config {
models: ModelsConfig::default(),
agent: NativeAgentConfig::default(),
experimental: ExperimentalFeatures::default(),
content_creator: ContentCreatorConfig::default(),
navigation: NavigationConfig::default(),
}
}
}
+47 -2
View File
@@ -4,7 +4,7 @@
* 类似 cherry-studio 的图标导航栏,始终显示在应用左侧
*/
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import styled from "styled-components";
import {
Tooltip,
@@ -30,6 +30,7 @@ import {
import * as LucideIcons from "lucide-react";
import { getPluginsForSurface, PluginUIInfo } from "@/lib/api/pluginUI";
import { Page, PageParams } from "@/types/page";
import { getConfig } from "@/hooks/useTauri";
interface AppSidebarProps {
currentPage: Page;
@@ -143,6 +144,15 @@ function getIconByName(iconName: string): LucideIcon {
return IconComponent || Activity;
}
/** 默认启用的导航模块 */
const DEFAULT_ENABLED_NAV_ITEMS = [
"agent",
"projects",
"image-gen",
"api-server",
"provider-pool",
];
export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
const [theme, setTheme] = useState<"light" | "dark">(() => {
if (typeof window !== "undefined") {
@@ -153,11 +163,46 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
return "light";
});
// 启用的导航模块
const [enabledNavItems, setEnabledNavItems] = useState<string[]>(
DEFAULT_ENABLED_NAV_ITEMS,
);
// 已安装的侧边栏插件列表
const [sidebarPlugins, setSidebarPlugins] = useState<PluginUIInfo[]>([]);
// 刷新触发器
const [refreshTrigger, setRefreshTrigger] = useState(0);
// 加载导航配置
useEffect(() => {
const loadNavConfig = async () => {
try {
const config = await getConfig();
setEnabledNavItems(
config.navigation?.enabled_items || DEFAULT_ENABLED_NAV_ITEMS,
);
} catch (error) {
console.error("加载导航配置失败:", error);
}
};
loadNavConfig();
// 监听导航配置变更事件
const handleNavConfigChange = () => {
loadNavConfig();
};
window.addEventListener("nav-config-changed", handleNavConfigChange);
return () => {
window.removeEventListener("nav-config-changed", handleNavConfigChange);
};
}, []);
// 过滤后的导航项
const filteredMenuItems = useMemo(() => {
return mainMenuItems.filter((item) => enabledNavItems.includes(item.id));
}, [enabledNavItems]);
// 加载侧边栏插件
useEffect(() => {
const loadSidebarPlugins = async () => {
@@ -221,7 +266,7 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
</Tooltip>
<MenusContainer>
{mainMenuItems.map((item) => (
{filteredMenuItems.map((item) => (
<Tooltip key={item.id}>
<TooltipTrigger asChild>
<IconButton
@@ -1,5 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import styled, { keyframes, css } from "styled-components";
import {
ArrowRight,
@@ -12,61 +11,11 @@ import {
ChevronDown,
Search,
Globe,
GraduationCap,
Zap,
RefreshCw,
LayoutTemplate,
Music,
} from "lucide-react";
/**
* 创作模式类型
* 不同模式下 AI 的角色和用户参与度不同
*/
export type CreationMode = "guided" | "fast" | "hybrid" | "framework";
/**
* 模式配置
*/
export const CREATION_MODE_CONFIG: Record<
CreationMode,
{
name: string;
icon: React.ReactNode;
aiRole: string;
userInvolvement: "high" | "medium" | "low";
description: string;
}
> = {
guided: {
name: "引导模式",
icon: <GraduationCap className="w-4 h-4" />,
aiRole: "教练(提问引导)",
userInvolvement: "high",
description: "追求真实性、个人经历类内容",
},
fast: {
name: "快速模式",
icon: <Zap className="w-4 h-4" />,
aiRole: "助手(生成初稿)",
userInvolvement: "low",
description: "信息整理、快速产出",
},
hybrid: {
name: "混合模式",
icon: <RefreshCw className="w-4 h-4" />,
aiRole: "协作者(写框架)",
userInvolvement: "medium",
description: "平衡质量和效率",
},
framework: {
name: "框架模式",
icon: <LayoutTemplate className="w-4 h-4" />,
aiRole: "填充者(按框架生成)",
userInvolvement: "medium",
description: "固定格式文档(报告、标书)",
},
};
import { getConfig } from "@/hooks/useTauri";
import type { CreationMode } from "./types";
import { CREATION_MODE_CONFIG } from "./constants";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
@@ -351,7 +300,7 @@ interface EmptyStateProps {
}
// Scenarios Configuration - 与 ProjectType 统一
const CATEGORIES = [
const ALL_CATEGORIES = [
{
id: "general",
label: "通用对话",
@@ -379,6 +328,16 @@ const CATEGORIES = [
{ id: "novel", label: "小说创作", icon: <PenTool className="w-4 h-4" /> },
];
/** 默认启用的主题 */
const DEFAULT_ENABLED_THEMES = [
"general",
"social-media",
"poster",
"music",
"video",
"novel",
];
// 需要显示创作模式选择器的主题
const CREATION_THEMES = [
"social-media",
@@ -537,6 +496,44 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
onThemeChange,
onRecommendationClick,
}) => {
// 从配置中读取启用的主题
const [enabledThemes, setEnabledThemes] = useState<string[]>(
DEFAULT_ENABLED_THEMES,
);
// 加载配置
useEffect(() => {
const loadEnabledThemes = async () => {
try {
const config = await getConfig();
if (config.content_creator?.enabled_themes) {
setEnabledThemes(config.content_creator.enabled_themes);
}
} catch (e) {
console.error("加载主题配置失败:", e);
}
};
loadEnabledThemes();
// 监听主题配置变更事件
const handleThemeConfigChange = () => {
loadEnabledThemes();
};
window.addEventListener("theme-config-changed", handleThemeConfigChange);
return () => {
window.removeEventListener(
"theme-config-changed",
handleThemeConfigChange,
);
};
}, []);
// 过滤后的主题列表
const categories = ALL_CATEGORIES.filter((cat) =>
enabledThemes.includes(cat.id),
);
// 使用外部传入的 activeTheme,如果有 onThemeChange 则使用受控模式
const handleThemeChange = (theme: string) => {
if (onThemeChange) {
@@ -552,6 +549,9 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
const [ratio, setRatio] = useState("3:4");
const [style, setStyle] = useState("minimal");
const [depth, setDepth] = useState("deep");
// Popover 打开状态
const [ratioPopoverOpen, setRatioPopoverOpen] = useState(false);
const [stylePopoverOpen, setStylePopoverOpen] = useState(false);
const handleSend = () => {
if (!input.trim()) return;
@@ -635,7 +635,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
</Header>
<TabsContainer>
{CATEGORIES.map((cat) => (
{categories.map((cat) => (
<TabItem
key={cat.id}
$active={activeTheme === cat.id}
@@ -682,7 +682,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<span>{getPlatformLabel(platform)}</span>
</div>
</SelectTrigger>
<SelectContent className="p-1">
<SelectContent className="p-1" side="top">
<div className="px-2 py-1.5 text-xs text-muted-foreground font-medium">
选择要创作的内容平台
</div>
@@ -753,7 +753,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<span>{CREATION_MODE_CONFIG[creationMode].name}</span>
</div>
</SelectTrigger>
<SelectContent className="p-1 min-w-[200px]">
<SelectContent className="p-1 min-w-[200px]" side="top">
<div className="px-2 py-1.5 text-xs text-muted-foreground font-medium">
选择创作模式
</div>
@@ -788,7 +788,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<BrainCircuit className="w-3.5 h-3.5 mr-2 text-muted-foreground" />
<SelectValue placeholder="深度" />
</SelectTrigger>
<SelectContent>
<SelectContent side="top">
<SelectItem value="deep">深度解析</SelectItem>
<SelectItem value="quick">快速概览</SelectItem>
</SelectContent>
@@ -808,7 +808,13 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
{activeTheme === "poster" && (
<>
<Popover>
<Popover
open={ratioPopoverOpen}
onOpenChange={(open) => {
setRatioPopoverOpen(open);
if (open) setStylePopoverOpen(false);
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
@@ -822,7 +828,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<ChevronDown className="w-3 h-3 ml-1 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 p-2" align="start">
<PopoverContent
className="w-64 p-2 bg-background border shadow-lg"
align="start"
side="top"
>
<div className="text-xs font-medium mb-2 px-2 text-muted-foreground">
宽高比
</div>
@@ -832,7 +842,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<GridItem
key={r}
$active={ratio === r}
onClick={() => setRatio(r)}
onClick={() => {
setRatio(r);
setRatioPopoverOpen(false);
}}
>
<div className="w-5 h-5 border-2 border-current rounded-sm mb-1 opacity-50"></div>
<span className="text-xs">{r}</span>
@@ -843,7 +856,13 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
</PopoverContent>
</Popover>
<Popover>
<Popover
open={stylePopoverOpen}
onOpenChange={(open) => {
setStylePopoverOpen(open);
if (open) setRatioPopoverOpen(false);
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
@@ -859,13 +878,20 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
<ChevronDown className="w-3 h-3 ml-1 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-48 p-1" align="start">
<PopoverContent
className="w-48 p-1 bg-background border shadow-lg"
align="start"
side="top"
>
<div className="p-1">
<Button
variant="ghost"
size="sm"
className="w-full justify-start h-8"
onClick={() => setStyle("minimal")}
onClick={() => {
setStyle("minimal");
setStylePopoverOpen(false);
}}
>
<ColorDot $color="#e2e8f0" className="mr-2" />{" "}
极简风格
@@ -874,7 +900,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
variant="ghost"
size="sm"
className="w-full justify-start h-8"
onClick={() => setStyle("tech")}
onClick={() => {
setStyle("tech");
setStylePopoverOpen(false);
}}
>
<ColorDot $color="#3b82f6" className="mr-2" />{" "}
科技质感
@@ -883,7 +912,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
variant="ghost"
size="sm"
className="w-full justify-start h-8"
onClick={() => setStyle("warm")}
onClick={() => {
setStyle("warm");
setStylePopoverOpen(false);
}}
>
<ColorDot $color="#f59e0b" className="mr-2" />{" "}
温暖治愈
@@ -0,0 +1,46 @@
import React from "react";
import { GraduationCap, Zap, RefreshCw, LayoutTemplate } from "lucide-react";
import type { CreationMode } from "./types";
/**
* 模式配置
*/
export const CREATION_MODE_CONFIG: Record<
CreationMode,
{
name: string;
icon: React.ReactNode;
aiRole: string;
userInvolvement: "high" | "medium" | "low";
description: string;
}
> = {
guided: {
name: "引导模式",
icon: <GraduationCap className="w-4 h-4" />,
aiRole: "教练(提问引导)",
userInvolvement: "high",
description: "追求真实性、个人经历类内容",
},
fast: {
name: "快速模式",
icon: <Zap className="w-4 h-4" />,
aiRole: "助手(生成初稿)",
userInvolvement: "low",
description: "信息整理、快速产出",
},
hybrid: {
name: "混合模式",
icon: <RefreshCw className="w-4 h-4" />,
aiRole: "协作者(写框架)",
userInvolvement: "medium",
description: "平衡质量和效率",
},
framework: {
name: "框架模式",
icon: <LayoutTemplate className="w-4 h-4" />,
aiRole: "填充者(按框架生成)",
userInvolvement: "medium",
description: "固定格式文档(报告、标书)",
},
};
@@ -0,0 +1,5 @@
/**
* 创作模式类型
* 不同模式下 AI 的角色和用户参与度不同
*/
export type CreationMode = "guided" | "fast" | "hybrid" | "framework";
+2 -1
View File
@@ -16,7 +16,8 @@ import { ChatSidebar } from "./components/ChatSidebar";
import { ChatSettings } from "./components/ChatSettings";
import { MessageList } from "./components/MessageList";
import { Inputbar } from "./components/Inputbar";
import { EmptyState, CreationMode } from "./components/EmptyState";
import { EmptyState } from "./components/EmptyState";
import type { CreationMode } from "./components/types";
import { type TaskFile } from "./components/TaskFiles";
import { LayoutTransition } from "@/components/content-creator/core/LayoutTransition/LayoutTransition";
import { StepProgress } from "@/components/content-creator/core/StepGuide/StepProgress";
+58 -1
View File
@@ -4,8 +4,10 @@
* 显示项目类型过滤标签
*/
import { useState, useEffect, useMemo } from "react";
import { cn } from "@/lib/utils";
import { TYPE_CONFIGS, type UserType } from "@/lib/api/project";
import { getConfig } from "@/hooks/useTauri";
export type ProjectFilter = "all" | UserType | "favorites" | "archived";
@@ -15,7 +17,17 @@ interface ProjectCategoriesProps {
counts?: Record<ProjectFilter, number>;
}
const filterItems: { id: ProjectFilter; label: string; icon?: string }[] = [
/** 默认启用的主题 */
const DEFAULT_ENABLED_THEMES = [
"general",
"social-media",
"poster",
"music",
"video",
"novel",
];
const allFilterItems: { id: ProjectFilter; label: string; icon?: string }[] = [
{ id: "all", label: "全部" },
{
id: "general",
@@ -71,6 +83,51 @@ export function ProjectCategories({
onFilterChange,
counts,
}: ProjectCategoriesProps) {
// 从配置中读取启用的主题
const [enabledThemes, setEnabledThemes] = useState<string[]>(
DEFAULT_ENABLED_THEMES,
);
// 加载配置
useEffect(() => {
const loadEnabledThemes = async () => {
try {
const config = await getConfig();
if (config.content_creator?.enabled_themes) {
setEnabledThemes(config.content_creator.enabled_themes);
}
} catch (e) {
console.error("加载主题配置失败:", e);
}
};
loadEnabledThemes();
// 监听主题配置变更事件
const handleThemeConfigChange = () => {
loadEnabledThemes();
};
window.addEventListener("theme-config-changed", handleThemeConfigChange);
return () => {
window.removeEventListener(
"theme-config-changed",
handleThemeConfigChange,
);
};
}, []);
// 过滤后的标签列表
const filterItems = useMemo(() => {
return allFilterItems.filter((item) => {
// all, favorites, archived 始终显示
if (["all", "favorites", "archived"].includes(item.id)) {
return true;
}
// 其他根据配置过滤
return enabledThemes.includes(item.id);
});
}, [enabledThemes]);
return (
<div className="flex items-center gap-2 flex-wrap">
{filterItems.map((item) => {
@@ -22,7 +22,6 @@ import {
HeartOff,
RotateCcw,
Activity,
Download,
} from "lucide-react";
import { useProviderPool } from "@/hooks/useProviderPool";
import { useApiKeyProvider } from "@/hooks/useApiKeyProvider";
@@ -32,7 +31,6 @@ import { AddCredentialModal } from "./AddCredentialModal";
import { EditCredentialModal } from "./EditCredentialModal";
import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { getConfig } from "@/hooks/useTauri";
import { ProviderIcon } from "@/icons/providers";
import { ApiKeyProviderSection, AddCustomProviderModal } from "./api-key";
import type { ApiKeyProviderSectionRef } from "./api-key";
@@ -91,7 +89,8 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingCredential, setEditingCredential] =
useState<CredentialDisplay | null>(null);
const [activeCategory, setActiveCategory] = useState<CategoryType>("oauth");
const [activeCategory, setActiveCategory] =
useState<CategoryType>("apikey");
const [activeTab, setActiveTab] = useState<TabType>("kiro");
const [deletingCredentials, setDeletingCredentials] = useState<Set<string>>(
new Set(),
@@ -121,7 +120,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
checkTypeHealth,
refreshCredentialToken,
updateCredential,
migratePrivateConfig,
} = useProviderPool();
// API Key Provider Hook
@@ -131,7 +129,7 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
refresh: refreshApiKeyProviders,
} = useApiKeyProvider();
const [migrating, setMigrating] = useState(false);
const [_migrating, setMigrating] = useState(false);
// Kiro 本地活跃凭证 UUID
const [localActiveUuid, setLocalActiveUuid] = useState<string | null>(null);
@@ -241,11 +239,16 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
};
// 迁移 Private 配置到凭证池
const handleMigratePrivateConfig = async () => {
const _handleMigratePrivateConfig = async () => {
setMigrating(true);
try {
const config = await getConfig();
const result = await migratePrivateConfig(config);
// const config = await getConfig();
// const result = await migratePrivateConfig(config);
const result = {
migrated_count: 0,
skipped_count: 0,
errors: [] as string[],
};
if (result.migrated_count > 0) {
showSuccess(
`成功迁移 ${result.migrated_count} 个凭证${result.skipped_count > 0 ? `,跳过 ${result.skipped_count} 个已存在的凭证` : ""}`,
@@ -351,17 +354,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
Provider 后自动使用对应凭证
</p>
</div>
<button
onClick={handleMigratePrivateConfig}
disabled={migrating || loading}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
title="从高级设置导入 Private 凭证"
>
<Download
className={`h-4 w-4 ${migrating ? "animate-pulse" : ""}`}
/>
导入配置
</button>
</div>
{error && (
@@ -372,20 +364,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
{/* Category Tabs - 第一行:分类选择 */}
<div className="flex gap-1 mb-2">
<button
onClick={() => {
setActiveCategory("oauth");
setActiveTab(oauthProviderTypes[0]);
}}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
activeCategory === "oauth"
? "border-primary bg-primary/10 text-primary"
: "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
data-testid="oauth-category-tab"
>
OAuth 凭证
</button>
<button
onClick={() => {
setActiveCategory("apikey");
@@ -439,6 +417,20 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
>
语音服务
</button>
<button
onClick={() => {
setActiveCategory("oauth");
setActiveTab(oauthProviderTypes[0]);
}}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
activeCategory === "oauth"
? "border-primary bg-primary/10 text-primary"
: "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
data-testid="oauth-category-tab"
>
OAuth 凭证
</button>
</div>
{/* OAuth 凭证分类 - Provider 选择图标网格 */}
+169
View File
@@ -11,6 +11,7 @@ import {
Info,
RotateCcw,
Volume2,
Palette,
} from "lucide-react";
import { cn, validateProxyUrl } from "@/lib/utils";
import { getConfig, saveConfig, Config } from "@/hooks/useTauri";
@@ -21,11 +22,61 @@ import { useSoundContext } from "@/contexts/useSoundContext";
type Theme = "light" | "dark" | "system";
/** 所有可用的内容创作主题 */
const ALL_CONTENT_THEMES = [
{ id: "general", label: "通用" },
{ id: "social-media", label: "社媒内容" },
{ id: "poster", label: "图文海报" },
{ id: "music", label: "歌词曲谱" },
{ id: "video", label: "短视频" },
{ id: "novel", label: "小说" },
{ id: "knowledge", label: "知识探索" },
{ id: "planning", label: "计划规划" },
{ id: "document", label: "办公文档" },
] as const;
/** 默认启用的主题 */
const DEFAULT_ENABLED_THEMES = [
"general",
"social-media",
"poster",
"music",
"video",
"novel",
];
/** 所有可用的导航模块 */
const ALL_NAV_ITEMS = [
{ id: "agent", label: "AI Agent" },
{ id: "projects", label: "项目" },
{ id: "image-gen", label: "图片生成" },
{ id: "api-server", label: "API Server" },
{ id: "provider-pool", label: "凭证池" },
{ id: "terminal", label: "终端" },
{ id: "tools", label: "工具" },
{ id: "plugins", label: "插件中心" },
] as const;
/** 默认启用的导航模块 */
const DEFAULT_ENABLED_NAV_ITEMS = [
"agent",
"projects",
"image-gen",
"api-server",
"provider-pool",
];
export function GeneralSettings() {
const [theme, setTheme] = useState<Theme>("system");
const [launchOnStartup, setLaunchOnStartup] = useState(false);
const [minimizeToTray, setMinimizeToTray] = useState(true);
const [language, setLanguageState] = useState<Language>("zh");
const [enabledThemes, setEnabledThemes] = useState<string[]>(
DEFAULT_ENABLED_THEMES,
);
const [enabledNavItems, setEnabledNavItems] = useState<string[]>(
DEFAULT_ENABLED_NAV_ITEMS,
);
const { resetOnboarding } = useOnboardingState();
const { setLanguage: setI18nLanguage } = useI18nPatch();
const { soundEnabled, setSoundEnabled, playToolcallSound } =
@@ -64,6 +115,12 @@ export function GeneralSettings() {
setProxyUrl(c.proxy_url || "");
setMinimizeToTray(c.minimize_to_tray ?? true);
setLanguageState((c.language || "zh") as Language);
setEnabledThemes(
c.content_creator?.enabled_themes || DEFAULT_ENABLED_THEMES,
);
setEnabledNavItems(
c.navigation?.enabled_items || DEFAULT_ENABLED_NAV_ITEMS,
);
} catch (e) {
console.error("加载配置失败:", e);
} finally {
@@ -130,6 +187,60 @@ export function GeneralSettings() {
}
};
// 切换内容创作主题
const handleThemeToggle = async (themeId: string) => {
if (!config) return;
const newThemes = enabledThemes.includes(themeId)
? enabledThemes.filter((t) => t !== themeId)
: [...enabledThemes, themeId];
// 至少保留一个主题
if (newThemes.length === 0) return;
setEnabledThemes(newThemes);
try {
const newConfig = {
...config,
content_creator: { enabled_themes: newThemes },
};
await saveConfig(newConfig);
setConfig(newConfig);
// 触发内容创作主题更新
window.dispatchEvent(new CustomEvent("theme-config-changed"));
} catch (err) {
console.error("保存主题设置失败:", err);
// 回滚
setEnabledThemes(enabledThemes);
}
};
// 切换导航模块
const handleNavItemToggle = async (itemId: string) => {
if (!config) return;
const newItems = enabledNavItems.includes(itemId)
? enabledNavItems.filter((i) => i !== itemId)
: [...enabledNavItems, itemId];
// 至少保留一个模块
if (newItems.length === 0) return;
setEnabledNavItems(newItems);
try {
const newConfig = {
...config,
navigation: { enabled_items: newItems },
};
await saveConfig(newConfig);
setConfig(newConfig);
// 触发侧边栏更新
window.dispatchEvent(new CustomEvent("nav-config-changed"));
} catch (err) {
console.error("保存导航设置失败:", err);
// 回滚
setEnabledNavItems(enabledNavItems);
}
};
const themeOptions = [
{ id: "light" as Theme, label: "浅色", icon: Sun },
{ id: "dark" as Theme, label: "深色", icon: Moon },
@@ -291,6 +402,64 @@ export function GeneralSettings() {
</label>
</div>
{/* 内容创作主题 */}
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 mb-3">
<Palette className="h-4 w-4 text-muted-foreground" />
<div>
<h3 className="text-sm font-medium">内容创作主题</h3>
<p className="text-xs text-muted-foreground">
选择在内容创作模式中显示的主题标签
</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{ALL_CONTENT_THEMES.map((t) => (
<button
key={t.id}
onClick={() => handleThemeToggle(t.id)}
className={cn(
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
enabledThemes.includes(t.id)
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80",
)}
>
{t.label}
</button>
))}
</div>
</div>
{/* 导航模块 */}
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 mb-3">
<Palette className="h-4 w-4 text-muted-foreground" />
<div>
<h3 className="text-sm font-medium">导航模块</h3>
<p className="text-xs text-muted-foreground">
选择在左侧导航栏中显示的功能模块
</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{ALL_NAV_ITEMS.map((item) => (
<button
key={item.id}
onClick={() => handleNavItemToggle(item.id)}
className={cn(
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
enabledNavItems.includes(item.id)
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80",
)}
>
{item.label}
</button>
))}
</div>
</div>
{/* 重新运行引导 */}
<div className="rounded-lg border p-3">
<div className="flex items-center justify-between">
+4 -1
View File
@@ -101,11 +101,13 @@ const SelectValue: React.FC<SelectValueProps> = ({ placeholder }) => {
interface SelectContentProps {
className?: string;
children: React.ReactNode;
side?: "top" | "bottom";
}
const SelectContent: React.FC<SelectContentProps> = ({
className,
children,
side = "bottom",
}) => {
const context = useContext(SelectContext);
if (!context) throw new Error("SelectContent must be used within Select");
@@ -117,7 +119,8 @@ const SelectContent: React.FC<SelectContentProps> = ({
return (
<div
className={cn(
"absolute top-full z-50 w-full rounded-md border bg-white shadow-md",
"absolute z-50 w-full rounded-md border bg-background shadow-lg",
side === "top" ? "bottom-full mb-1" : "top-full mt-1",
className,
)}
>
+20
View File
@@ -129,6 +129,22 @@ export interface ExperimentalFeatures {
screenshot_chat: SmartInputConfig;
}
/**
* 内容创作配置
*/
export interface ContentCreatorConfig {
/** 启用的主题列表 */
enabled_themes: string[];
}
/**
* 导航栏配置
*/
export interface NavigationConfig {
/** 启用的导航模块列表 */
enabled_items: string[];
}
export interface Config {
server: {
host: string;
@@ -173,6 +189,10 @@ export interface Config {
language: string;
/** 实验室功能配置 */
experimental?: ExperimentalFeatures;
/** 内容创作配置 */
content_creator?: ContentCreatorConfig;
/** 导航栏配置 */
navigation?: NavigationConfig;
}
export interface LogEntry {