From 771a71436f6ee986b13ef41e10c6b99c9f373172 Mon Sep 17 00:00:00 2001 From: PoRi <1960825664@qq.com> Date: Sat, 25 Apr 2026 18:39:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E4=B8=8B=E8=BD=BD=E8=B7=AF=E5=BE=84=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 get_default_save_dir Tauri 命令用于获取默认保存目录 - 修改 download_model 命令支持自定义保存路径参数 - 在模型页面集成本地存储的保存路径配置 - 添加 MSW 到 pnpm 工作区允许构建列表 feat(ui): 添加模型设置页面 - 创建新的设置页面用于配置模型下载路径 - 实现路径选择、保存和重置功能 - 集成到侧边栏导航菜单中 - 使用 localStorage 持久化保存用户配置 refactor(routes): 重构设置页面路由结构 - 移除旧的侧边栏导航组件 - 更新路由配置以支持新的模型设置页面 - 生成相应的路由类型定义更新 feat(server): 添加 OpenAI 兼容的模型接口 - 在 /v1 路径下添加 models 接口端点 - 提供与 OpenAI API 兼容的模型列表功能 --- aha-ui/pnpm-workspace.yaml | 1 + aha-ui/src-tauri/src/lib.rs | 13 +++- .../components/layout/data/sidebar-data.ts | 6 ++ aha-ui/src/features/models/index.tsx | 5 +- aha-ui/src/features/settings/index.tsx | 39 +--------- aha-ui/src/features/settings/model/index.tsx | 74 +++++++++++++++++++ aha-ui/src/routeTree.gen.ts | 22 ++++++ .../routes/_authenticated/settings/model.tsx | 6 ++ src/server/mod.rs | 2 + 9 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 aha-ui/src/features/settings/model/index.tsx create mode 100644 aha-ui/src/routes/_authenticated/settings/model.tsx diff --git a/aha-ui/pnpm-workspace.yaml b/aha-ui/pnpm-workspace.yaml index 5ed0b5a..220bd0b 100644 --- a/aha-ui/pnpm-workspace.yaml +++ b/aha-ui/pnpm-workspace.yaml @@ -1,2 +1,3 @@ allowBuilds: esbuild: true + msw: true diff --git a/aha-ui/src-tauri/src/lib.rs b/aha-ui/src-tauri/src/lib.rs index 1fb9168..ad17446 100644 --- a/aha-ui/src-tauri/src/lib.rs +++ b/aha-ui/src-tauri/src/lib.rs @@ -60,6 +60,11 @@ struct ServerStatusResponse { // ── Helpers ────────────────────────────────────────── +#[tauri::command] +fn get_default_save_dir() -> Result { + aha::utils::get_default_save_dir().ok_or_else(|| "无法获取 home 目录".to_string()) +} + fn get_save_dir() -> Result { aha::utils::get_default_save_dir().ok_or_else(|| "无法获取 home 目录".to_string()) } @@ -206,10 +211,13 @@ fn get_model_detail(model_id: String) -> Result { } #[tauri::command] -async fn download_model(model_id: String) -> Result<(), String> { +async fn download_model(model_id: String, save_dir: Option) -> Result<(), String> { use aha::utils::download_model; - let save_dir = get_save_dir()?; + let save_dir = match save_dir { + Some(dir) => dir, + None => get_save_dir()?, + }; download_model(&model_id, &save_dir, 3) .await .map_err(|e| format!("下载失败: {}", e)) @@ -417,6 +425,7 @@ pub fn run() { stop_server, get_server_status, clear_logs, + get_default_save_dir, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/aha-ui/src/components/layout/data/sidebar-data.ts b/aha-ui/src/components/layout/data/sidebar-data.ts index e99bcfc..b559603 100644 --- a/aha-ui/src/components/layout/data/sidebar-data.ts +++ b/aha-ui/src/components/layout/data/sidebar-data.ts @@ -8,6 +8,7 @@ import { Palette, Bell, Monitor, + } from 'lucide-react' import { type SidebarData } from '../types' @@ -52,6 +53,11 @@ export const sidebarData: SidebarData = { title: 'Settings', icon: Settings, items: [ + { + title: 'Model', + url: '/settings/model', + icon: Package, + }, { title: 'Profile', url: '/settings', diff --git a/aha-ui/src/features/models/index.tsx b/aha-ui/src/features/models/index.tsx index f6824a6..b69bb00 100644 --- a/aha-ui/src/features/models/index.tsx +++ b/aha-ui/src/features/models/index.tsx @@ -9,6 +9,8 @@ import { Main } from "@/components/layout/main" import { ProfileDropdown } from "@/components/profile-dropdown" import { ThemeSwitch } from "@/components/theme-switch" +const SAVE_DIR_KEY = "aha-model-save-dir" + interface ModelInfo { model_id: string owner: string @@ -67,7 +69,8 @@ export function ModelsPage() { setDownloading(modelId) setError(null) try { - await invoke("download_model", { modelId }) + const saveDir = localStorage.getItem(SAVE_DIR_KEY) || null + await invoke("download_model", { modelId, saveDir }) await loadModels() } catch (e) { setError(String(e)) diff --git a/aha-ui/src/features/settings/index.tsx b/aha-ui/src/features/settings/index.tsx index a1f4937..0842b51 100644 --- a/aha-ui/src/features/settings/index.tsx +++ b/aha-ui/src/features/settings/index.tsx @@ -1,5 +1,4 @@ import { Outlet } from '@tanstack/react-router' -import { Monitor, Bell, Palette, Wrench, UserCog } from 'lucide-react' import { Separator } from '@/components/ui/separator' import { ConfigDrawer } from '@/components/config-drawer' import { Header } from '@/components/layout/header' @@ -7,35 +6,6 @@ import { Main } from '@/components/layout/main' import { ProfileDropdown } from '@/components/profile-dropdown' import { Search } from '@/components/search' import { ThemeSwitch } from '@/components/theme-switch' -import { SidebarNav } from './components/sidebar-nav' - -const sidebarNavItems = [ - { - title: 'Profile', - href: '/settings', - icon: , - }, - { - title: 'Account', - href: '/settings/account', - icon: , - }, - { - title: 'Appearance', - href: '/settings/appearance', - icon: , - }, - { - title: 'Notifications', - href: '/settings/notifications', - icon: , - }, - { - title: 'Display', - href: '/settings/display', - icon: , - }, -] export function Settings() { return ( @@ -58,13 +28,8 @@ export function Settings() {

-
- -
- -
+
+
diff --git a/aha-ui/src/features/settings/model/index.tsx b/aha-ui/src/features/settings/model/index.tsx new file mode 100644 index 0000000..f61467c --- /dev/null +++ b/aha-ui/src/features/settings/model/index.tsx @@ -0,0 +1,74 @@ +import { useState, useEffect } from "react" +import { invoke } from "@tauri-apps/api/core" +import { FolderOpen } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { ContentSection } from "../components/content-section" + +const SAVE_DIR_KEY = "aha-model-save-dir" + +export function SettingsModel() { + const [saveDir, setSaveDir] = useState("") + const [defaultDir, setDefaultDir] = useState("") + const [saved, setSaved] = useState(false) + + useEffect(() => { + invoke("get_default_save_dir") + .then(setDefaultDir) + .catch(() => {}) + + const stored = localStorage.getItem(SAVE_DIR_KEY) + if (stored) setSaveDir(stored) + }, []) + + const handleSave = () => { + if (saveDir) { + localStorage.setItem(SAVE_DIR_KEY, saveDir) + } else { + localStorage.removeItem(SAVE_DIR_KEY) + } + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } + + const handleReset = () => { + setSaveDir("") + localStorage.removeItem(SAVE_DIR_KEY) + } + + return ( + +
+
+ + setSaveDir(e.target.value)} + placeholder={defaultDir || "~/.aha/"} + /> +

+ 留空则使用默认路径:{defaultDir || "~/.aha/"} + 。模型将下载到该目录下的 {`{model_id}`} 子文件夹中。 +

+
+ +
+ + +
+
+
+ ) +} diff --git a/aha-ui/src/routeTree.gen.ts b/aha-ui/src/routeTree.gen.ts index 41674f7..38506a1 100644 --- a/aha-ui/src/routeTree.gen.ts +++ b/aha-ui/src/routeTree.gen.ts @@ -26,6 +26,7 @@ import { Route as authForgotPasswordRouteImport } from './routes/(auth)/forgot-p import { Route as AuthenticatedSettingsRouteRouteImport } from './routes/_authenticated/settings/route' import { Route as AuthenticatedSettingsIndexRouteImport } from './routes/_authenticated/settings/index' import { Route as AuthenticatedSettingsNotificationsRouteImport } from './routes/_authenticated/settings/notifications' +import { Route as AuthenticatedSettingsModelRouteImport } from './routes/_authenticated/settings/model' import { Route as AuthenticatedSettingsDisplayRouteImport } from './routes/_authenticated/settings/display' import { Route as AuthenticatedSettingsAppearanceRouteImport } from './routes/_authenticated/settings/appearance' import { Route as AuthenticatedSettingsAccountRouteImport } from './routes/_authenticated/settings/account' @@ -117,6 +118,12 @@ const AuthenticatedSettingsNotificationsRoute = path: '/notifications', getParentRoute: () => AuthenticatedSettingsRouteRoute, } as any) +const AuthenticatedSettingsModelRoute = + AuthenticatedSettingsModelRouteImport.update({ + id: '/model', + path: '/model', + getParentRoute: () => AuthenticatedSettingsRouteRoute, + } as any) const AuthenticatedSettingsDisplayRoute = AuthenticatedSettingsDisplayRouteImport.update({ id: '/display', @@ -154,6 +161,7 @@ export interface FileRoutesByFullPath { '/settings/account': typeof AuthenticatedSettingsAccountRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/settings/model': typeof AuthenticatedSettingsModelRoute '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/settings/': typeof AuthenticatedSettingsIndexRoute } @@ -174,6 +182,7 @@ export interface FileRoutesByTo { '/settings/account': typeof AuthenticatedSettingsAccountRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/settings/model': typeof AuthenticatedSettingsModelRoute '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/settings': typeof AuthenticatedSettingsIndexRoute } @@ -197,6 +206,7 @@ export interface FileRoutesById { '/_authenticated/settings/account': typeof AuthenticatedSettingsAccountRoute '/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute '/_authenticated/settings/display': typeof AuthenticatedSettingsDisplayRoute + '/_authenticated/settings/model': typeof AuthenticatedSettingsModelRoute '/_authenticated/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute '/_authenticated/settings/': typeof AuthenticatedSettingsIndexRoute } @@ -220,6 +230,7 @@ export interface FileRouteTypes { | '/settings/account' | '/settings/appearance' | '/settings/display' + | '/settings/model' | '/settings/notifications' | '/settings/' fileRoutesByTo: FileRoutesByTo @@ -240,6 +251,7 @@ export interface FileRouteTypes { | '/settings/account' | '/settings/appearance' | '/settings/display' + | '/settings/model' | '/settings/notifications' | '/settings' id: @@ -262,6 +274,7 @@ export interface FileRouteTypes { | '/_authenticated/settings/account' | '/_authenticated/settings/appearance' | '/_authenticated/settings/display' + | '/_authenticated/settings/model' | '/_authenticated/settings/notifications' | '/_authenticated/settings/' fileRoutesById: FileRoutesById @@ -401,6 +414,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedSettingsNotificationsRouteImport parentRoute: typeof AuthenticatedSettingsRouteRoute } + '/_authenticated/settings/model': { + id: '/_authenticated/settings/model' + path: '/model' + fullPath: '/settings/model' + preLoaderRoute: typeof AuthenticatedSettingsModelRouteImport + parentRoute: typeof AuthenticatedSettingsRouteRoute + } '/_authenticated/settings/display': { id: '/_authenticated/settings/display' path: '/display' @@ -429,6 +449,7 @@ interface AuthenticatedSettingsRouteRouteChildren { AuthenticatedSettingsAccountRoute: typeof AuthenticatedSettingsAccountRoute AuthenticatedSettingsAppearanceRoute: typeof AuthenticatedSettingsAppearanceRoute AuthenticatedSettingsDisplayRoute: typeof AuthenticatedSettingsDisplayRoute + AuthenticatedSettingsModelRoute: typeof AuthenticatedSettingsModelRoute AuthenticatedSettingsNotificationsRoute: typeof AuthenticatedSettingsNotificationsRoute AuthenticatedSettingsIndexRoute: typeof AuthenticatedSettingsIndexRoute } @@ -438,6 +459,7 @@ const AuthenticatedSettingsRouteRouteChildren: AuthenticatedSettingsRouteRouteCh AuthenticatedSettingsAccountRoute: AuthenticatedSettingsAccountRoute, AuthenticatedSettingsAppearanceRoute: AuthenticatedSettingsAppearanceRoute, AuthenticatedSettingsDisplayRoute: AuthenticatedSettingsDisplayRoute, + AuthenticatedSettingsModelRoute: AuthenticatedSettingsModelRoute, AuthenticatedSettingsNotificationsRoute: AuthenticatedSettingsNotificationsRoute, AuthenticatedSettingsIndexRoute: AuthenticatedSettingsIndexRoute, diff --git a/aha-ui/src/routes/_authenticated/settings/model.tsx b/aha-ui/src/routes/_authenticated/settings/model.tsx new file mode 100644 index 0000000..a3bf1bb --- /dev/null +++ b/aha-ui/src/routes/_authenticated/settings/model.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router' +import { SettingsModel } from '@/features/settings/model' + +export const Route = createFileRoute('/_authenticated/settings/model')({ + component: SettingsModel, +}) diff --git a/src/server/mod.rs b/src/server/mod.rs index c01a93a..5c881e1 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -72,6 +72,8 @@ pub(crate) async fn start_http_server( // Health check and model info endpoints builder = builder.mount("/", routes![api::health, api::models]); + // OpenAI-compatible model listing endpoint: /v1/models + builder = builder.mount("/v1", routes![api::models]); // Shutdown endpoint builder = builder.manage(shutdown_flag); builder = builder.mount("/", routes![api::shutdown]);