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]);