From 04b80a616eda444fa9f465f95a72fb361ea8ec94 Mon Sep 17 00:00:00 2001 From: ekko Date: Thu, 16 Apr 2026 09:40:25 +0800 Subject: [PATCH 1/4] feat: add profile management page with full CRUD UI - Add frontend API layer, Pinia store, and 5 components (ProfileCard, ProfilesPanel, ProfileCreateModal, ProfileRenameModal, ProfileImportModal) - Add ProfilesView page with card grid layout and expandable details - Modify export endpoint to stream file as browser download instead of returning server path - Add sidebar nav entry, router route, and i18n translations (en/zh) - Support create, rename, delete, switch (with gateway restart), export, and import profiles Co-Authored-By: Claude Opus 4.6 --- packages/client/src/api/hermes/profiles.ts | 113 +++++++ .../hermes/profiles/ProfileCard.vue | 290 ++++++++++++++++++ .../hermes/profiles/ProfileCreateModal.vue | 87 ++++++ .../hermes/profiles/ProfileImportModal.vue | 92 ++++++ .../hermes/profiles/ProfileRenameModal.vue | 83 +++++ .../hermes/profiles/ProfilesPanel.vue | 56 ++++ .../src/components/layout/AppSidebar.vue | 21 ++ packages/client/src/i18n/locales/en.ts | 48 +++ packages/client/src/i18n/locales/zh.ts | 48 +++ packages/client/src/router/index.ts | 5 + packages/client/src/stores/hermes/profiles.ts | 96 ++++++ .../client/src/views/hermes/ProfilesView.vue | 120 ++++++++ packages/server/src/routes/hermes/profiles.ts | 26 +- 13 files changed, 1081 insertions(+), 4 deletions(-) create mode 100644 packages/client/src/api/hermes/profiles.ts create mode 100644 packages/client/src/components/hermes/profiles/ProfileCard.vue create mode 100644 packages/client/src/components/hermes/profiles/ProfileCreateModal.vue create mode 100644 packages/client/src/components/hermes/profiles/ProfileImportModal.vue create mode 100644 packages/client/src/components/hermes/profiles/ProfileRenameModal.vue create mode 100644 packages/client/src/components/hermes/profiles/ProfilesPanel.vue create mode 100644 packages/client/src/stores/hermes/profiles.ts create mode 100644 packages/client/src/views/hermes/ProfilesView.vue diff --git a/packages/client/src/api/hermes/profiles.ts b/packages/client/src/api/hermes/profiles.ts new file mode 100644 index 0000000..2073966 --- /dev/null +++ b/packages/client/src/api/hermes/profiles.ts @@ -0,0 +1,113 @@ +import { request, getBaseUrlValue, getApiKey } from '../client' + +export interface HermesProfile { + name: string + active: boolean + model: string + gateway: string + alias: string +} + +export interface HermesProfileDetail { + name: string + path: string + model: string + provider: string + gateway: string + skills: number + hasEnv: boolean + hasSoulMd: boolean +} + +export async function fetchProfiles(): Promise { + const res = await request<{ profiles: HermesProfile[] }>('/api/hermes/profiles') + return res.profiles +} + +export async function fetchProfileDetail(name: string): Promise { + const res = await request<{ profile: HermesProfileDetail }>(`/api/hermes/profiles/${encodeURIComponent(name)}`) + return res.profile +} + +export async function createProfile(name: string, clone?: boolean): Promise { + try { + await request('/api/hermes/profiles', { + method: 'POST', + body: JSON.stringify({ name, clone }), + }) + return true + } catch { + return false + } +} + +export async function deleteProfile(name: string): Promise { + try { + await request(`/api/hermes/profiles/${encodeURIComponent(name)}`, { method: 'DELETE' }) + return true + } catch { + return false + } +} + +export async function renameProfile(name: string, newName: string): Promise { + try { + await request(`/api/hermes/profiles/${encodeURIComponent(name)}/rename`, { + method: 'POST', + body: JSON.stringify({ new_name: newName }), + }) + return true + } catch { + return false + } +} + +export async function switchProfile(name: string): Promise { + try { + await request('/api/hermes/profiles/active', { + method: 'PUT', + body: JSON.stringify({ name }), + }) + return true + } catch { + return false + } +} + +export async function exportProfile(name: string): Promise { + try { + const baseUrl = getBaseUrlValue() + const token = getApiKey() + const headers: Record = {} + if (token) headers['Authorization'] = `Bearer ${token}` + + const res = await fetch(`${baseUrl}/api/hermes/profiles/${encodeURIComponent(name)}/export`, { + method: 'POST', + headers, + }) + if (!res.ok) throw new Error() + + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `hermes-profile-${name}.tar.gz` + a.click() + URL.revokeObjectURL(url) + return true + } catch { + return false + } +} + +export async function importProfile(archive: string, name?: string): Promise { + try { + await request('/api/hermes/profiles/import', { + method: 'POST', + body: JSON.stringify({ archive, name }), + }) + return true + } catch { + return false + } +} diff --git a/packages/client/src/components/hermes/profiles/ProfileCard.vue b/packages/client/src/components/hermes/profiles/ProfileCard.vue new file mode 100644 index 0000000..ce9ef52 --- /dev/null +++ b/packages/client/src/components/hermes/profiles/ProfileCard.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/packages/client/src/components/hermes/profiles/ProfileCreateModal.vue b/packages/client/src/components/hermes/profiles/ProfileCreateModal.vue new file mode 100644 index 0000000..1b23f11 --- /dev/null +++ b/packages/client/src/components/hermes/profiles/ProfileCreateModal.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/packages/client/src/components/hermes/profiles/ProfileImportModal.vue b/packages/client/src/components/hermes/profiles/ProfileImportModal.vue new file mode 100644 index 0000000..d9d3f94 --- /dev/null +++ b/packages/client/src/components/hermes/profiles/ProfileImportModal.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/client/src/components/hermes/profiles/ProfileRenameModal.vue b/packages/client/src/components/hermes/profiles/ProfileRenameModal.vue new file mode 100644 index 0000000..aa09dbb --- /dev/null +++ b/packages/client/src/components/hermes/profiles/ProfileRenameModal.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/packages/client/src/components/hermes/profiles/ProfilesPanel.vue b/packages/client/src/components/hermes/profiles/ProfilesPanel.vue new file mode 100644 index 0000000..2a1e3e9 --- /dev/null +++ b/packages/client/src/components/hermes/profiles/ProfilesPanel.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/packages/client/src/components/layout/AppSidebar.vue b/packages/client/src/components/layout/AppSidebar.vue index a0ef8e7..0f3d5fd 100644 --- a/packages/client/src/components/layout/AppSidebar.vue +++ b/packages/client/src/components/layout/AppSidebar.vue @@ -147,6 +147,27 @@ function handleNav(key: string) { {{ t("sidebar.models") }} + +