Release v0.5.9 (#435)

* fix: add missing i18n key and unify session data source (#408)

- Add `chat.sessionNotFound` translation key to all 8 locales
- Fix history page data source inconsistency:
  - Change `getHermesSession` to prioritize database over CLI
  - Now consistent with `listHermesSessions` behavior
  - Prevents "session in list but detail not found" issue
- Update CI workflow to trigger on base branch PRs
- Remove debug log from sessions-db

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: filter special characters and emoji in speech playback (#409)

- Update extractReadableText to filter special characters like *#
- Only keep common punctuation marks for speech synthesis
- Remove emoji, symbols, and special unicode characters
- Improve text-to-speech readability

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add drawer panel with mobile sidebar support and customizable button (#412)

* feat: add drawer panel with mobile sidebar support

- Add DrawerPanel component with Terminal and Files tabs
- Extract TerminalPanel and FilesPanel from existing views
- Add mobile sidebar toggle functionality with overlay
- Add rainbow breathing light effect to drawer button
- Remove Tools section from AppSidebar (Terminal/Files entries)
- Add i18n support for drawer and file tree
- Optimize mobile button layout and spacing
- Fix z-index hierarchy for proper layering
- Add responsive sidebar behavior (PC: always visible, Mobile: toggle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: customize drawer button with arc rainbow border

- Change drawer button to semi-circle shape贴着右边
- Add arrow icon pointing left (向左箭头)
- Add rainbow border from top to bottom through semi-circle arc
- Slow down animation from 4s to 8s for smoother effect
- Move drawer button wrapper to messages area only (not贯穿header和input)
- Add semi-transparent accent color background to button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve profile switching state sync issue (#414) (#415)

* fix: resolve profile switching state sync issue (#414)

Fix bug where switching to a different profile would still show the
old profile name in the UI and prevent switching back to default.

Root cause:
- Frontend relied entirely on fetchProfiles() return value to set
  activeProfileName
- Backend Hermes CLI may return stale active flag due to timing
  issues between profile use and profile list commands
- This caused frontend to display wrong profile and prevented
  switching back to default

Solution:
- Immediately set activeProfileName when switchProfile API succeeds
- Don't rely solely on listProfiles() result which may have stale data
- Use activeProfileName instead of activeProfile?.name in ProfileSelector

Changes:
- profiles store: Set activeProfileName immediately after successful switch
- ProfileSelector: Use activeProfileName computed property
- Add test to verify activeProfileName updates on switch

Fixes #414

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refine: improve error handling for profile switching failures

Add proper error handling for edge cases:
- If fetchProfiles() fails after successful switch, keep the updated
  activeProfileName (don't let fetchProfiles failure undo the switch)
- Add test cases to verify:
  1. API failure doesn't change state
  2. fetchProfiles failure doesn't affect successful switch

This ensures the UI remains consistent even when profile list refresh
fails after a successful profile switch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refine: add rollback mechanism for profile switching verification

Add backend verification after profile switch:
- Save old activeProfileName before setting new value
- After fetchProfiles, verify backend reports expected active profile
- If backend reports different profile, rollback frontend state and return false
- This handles edge case where API returns 200 but backend didn't actually switch

Test cases:
-  Normal switch: updates and verifies successfully
-  API failure: doesn't change state
-  fetchProfiles failure: assumes success (API returned 200)
-  Backend verification fails: rolls back to old profile

This ensures frontend state always matches backend reality, even in
edge cases where hermes profile use succeeded but gateway/cleanup
steps failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refine: add user feedback for profile operations

Improve user experience with success/error messages:
- ProfileSelector: Add error message when switch fails
- ProfileCard: Add success message before reload on switch
- ProfileSelector: Use async/await for better error handling
- ProfileCard: Add 500ms delay before reload to show success message

Before: Silent failures, no feedback
After: Clear success/error messages for all operations

Example feedback:
- Success: "已切换到配置 qinghe"
- Failure: "切换配置失败,网关可能需要手动重启"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: update frontend changelog for v0.5.7 (#419)

* docs: update frontend changelog for v0.5.7

- Update changelog.ts with v0.5.7 release date and changes
- Add i18n translation keys for all languages (en, zh, de, es, fr, ja, ko, pt)
- Include v0.5.7 changelog entries:
  - Optimize context compression and session sync
  - Add startup delays to prevent database race conditions

Changes:
- packages/client/src/data/changelog.ts: Update v0.5.7 entry
- packages/client/src/i18n/locales/*.ts: Add changelog translation section

This enables the changelog modal in the UI to display v0.5.7 release notes.

* feat: add v0.5.7 changelog translations to all supported languages

Add new_0_5_7_1, new_0_5_7_2, and new_0_5_7_3 changelog entries to all
locale files (en, zh, de, es, fr, ja, ko, pt) with proper translations
for each language.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove duplicate changelog sections causing syntax errors

Remove duplicate changelog object sections that were causing TypeScript
syntax errors in all locale files (en, zh, de, es, fr, ja, ko, pt).
The actual changelog entries are already correctly placed in the main
changelog section of each file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add v0.5.8 changelog and fix profile parsing issue

Add v0.5.8 changelog entries based on PRs merged since v0.5.7:
- Drawer panel with mobile sidebar support (#412)
- Profile switching state sync fix (#414)
- Speech playback special character filtering (#409)
- Missing i18n key and session data source unification (#408)
- Vite build optimization for faster Docker builds (#403)

Also fix issue #417: Profile names with long hyphenated names fail
to parse in profile list regex. Change \s{2,} to \s+ to handle
compressed column spacing when profile names are long.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove enter key submit from profile creation and rename modals

Remove @keyup.enter handlers from NInput components in:
- ProfileCreateModal: prevent accidental profile creation when pressing enter
- ProfileRenameModal: prevent accidental profile rename when pressing enter

Users must now explicitly click the confirm button to submit, preventing
unintended profile operations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: allow free text input for profile names

Remove frontend character filtering from profile creation and rename
modals. Users can now input any characters including spaces and
uppercase letters to test backend Hermes CLI validation.

Changes:
- ProfileCreateModal: Remove toLowerCase() and character filtering
- ProfileRenameModal: Remove toLowerCase() and character filtering
- Use v-model:value binding instead of :value with @input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: improve error handling for profile creation

Display backend error messages when profile creation fails instead of
generic "failed" message. This helps users understand why their
profile name was rejected (e.g., invalid characters).

Changes:
- API layer: Capture and return error messages from backend
- ProfileCreateModal: Display specific error message from backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add profile name validation with i18n support

Add client-side validation for profile names to prevent invalid input
before sending to backend. Only lowercase letters, numbers, underscores,
and hyphens are allowed.

Changes:
- ProfileCreateModal: Add input validation with real-time feedback
- ProfileRenameModal: Add input validation with real-time feedback
- Add nameValidation i18n key for all 8 languages
- Filter invalid characters on input and show warning message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: revert profile parsing regex changes

Revert the regex changes in hermes-cli.ts and gateway-manager.ts
back to requiring \s{2,} (at least 2 spaces). Since frontend now
validates profile names to only allow lowercase letters, numbers,
underscores, and hyphens, the relaxed regex is no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: revert profile parsing regex changes

Revert the regex changes in gateway-manager.ts and hermes-cli.ts
back to requiring \s{2,} (at least 2 spaces). Since frontend now
validates profile names to only allow lowercase letters, numbers,
underscores, and hyphens, the relaxed regex is no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: remove tooltip from drawer button

Remove the NTooltip wrapper from the floating drawer button.
The "Terminal & Files" tooltip is no longer shown on hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update assets images (#421)

Updated two asset images in the client package.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump version to 0.5.8

Release v0.5.8

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: improve profile list parsing to handle long profile names (#425)

Fixed issue #423 where long profile names caused parsing failures.

Changes:
- gateway-manager.ts: Use `.+?` instead of `\S+` to match profile names, allowing names that overflow table column width
- hermes-cli.ts: Use `\s+` instead of `\s{2,}` for first delimiter to handle cases where long profile names reduce spacing to 1 space

The regex now correctly parses profile output even when profile names are long enough to compress table formatting, ensuring all profiles appear in the UI regardless of name length.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add GitHub issue templates

Add structured issue templates to guide users when submitting issues:
- Bug Report template with version info, reproduction steps, and environment details
- Feature Request template with problem statement, solution, and priority
- General Issue template for questions that don't fit other categories
- Config to enable blank issues and provide contact links to documentation and discussions

Templates use YAML forms for better structure and validation of required fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: unify profile management across the application (#432)

This commit addresses long-standing profile inconsistency issues by establishing
`~/.hermes/active_profile` file as the single source of truth for all profile
operations throughout the application.

## Changes

### Backend (Server)

**1. profiles.ts - Enhanced profile switching**
- Switch from CLI polling to direct file verification (Hermes CLI writes synchronously)
- Verify `active_profile` file with quick retry (max 2 attempts × 100ms = 300ms)
- Update GatewayManager only after file verification succeeds
- Add comprehensive logging for debugging

**2. profiles.ts - Authoritative API responses**
- Override CLI's active flag with `active_profile` file in `list()` endpoint
- Add warning when CLI output differs from file (detects inconsistencies)
- Ensures API responses always match actual runtime state

**3. jobs.ts - Use authoritative profile source**
- `resolveProfile()` falls back to `getActiveProfileName()` when no profile in request
- Ensures jobs operate on correct profile even if frontend doesn't specify

**4. cron-history.ts - Fix run history to respect active profile**
- Changed from fixed `~/.hermes/cron/output/` to `getActiveProfileDir()/cron/output/`
- Run history now correctly switches with profile (e.g., `~/.hermes/profiles/hermes/cron/output/`)

**5. proxy-handler.ts - Add fallback to authoritative source**
- If no profile in request headers/query, read from `getActiveProfileName()`
- Prevents proxy from using wrong default profile

### Frontend (Client)

**1. api/client.ts - Simplified profile resolution**
- Prioritize `useProfilesStore().activeProfileName` over localStorage
- localStorage fallback only for early initialization

**2. api/hermes/chat.ts - Consistent profile resolution**
- Same pattern: store first, localStorage fallback only during init

**3. stores/session-browser-prefs.ts - Clean up fallback logic**
- Prioritize store, remove redundant localStorage read

## Problem Solved

Previously, multiple components had different ways of determining the active profile:
- CLI output (◆ marker) - could be stale
- GatewayManager memory - startup cache only
- localStorage - frontend cache
- Various fallbacks scattered across codebase

This caused inconsistencies where:
- Frontend showed one profile but API used another
- Jobs ran on wrong profile
- Run history displayed wrong data
- Profile switches appeared to fail (but actually succeeded)

## Solution

All components now derive the active profile from the same authoritative source:
- `~/.hermes/active_profile` file (written synchronously by `hermes profile use`)
- `getActiveProfileName()` function (reads the file)
- Single source of truth = no inconsistencies

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add v0.5.9 changelog entries (#434)

- Add unified profile management across the application
- Add GitHub issue and pull request templates

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-04 12:46:26 +08:00
committed by GitHub
parent 4a11511383
commit 46bc2cf12e
17 changed files with 140 additions and 12 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-web-ui",
"version": "0.5.8",
"version": "0.5.9",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model (Claude, GPT, Gemini, DeepSeek) web UI with Telegram, Discord, Slack, WhatsApp integration",
"repository": {
"type": "git",
+18 -1
View File
@@ -26,6 +26,23 @@ export function hasApiKey(): boolean {
return !!getApiKey()
}
/**
* Get current active profile name.
* Reads from store first (authoritative source), falls back to localStorage.
*/
function getActiveProfileName(): string | null {
try {
// Dynamic import to avoid circular dependency
const { useProfilesStore } = require('@/stores/hermes/profiles')
const store = useProfilesStore()
// Store is the source of truth - it's updated from /api/hermes/profiles
return store.activeProfileName
} catch {
// Fallback to localStorage if store is not available (e.g., during initialization)
return localStorage.getItem('hermes_active_profile_name')
}
}
export async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const base = getBaseUrl()
const url = `${base}${path}`
@@ -40,7 +57,7 @@ export async function request<T>(path: string, options: RequestInit = {}): Promi
}
// Inject active profile header for proxied gateway requests
const profileName = localStorage.getItem('hermes_active_profile_name')
const profileName = getActiveProfileName()
if (profileName && profileName !== 'default') {
headers['X-Hermes-Profile'] = profileName
}
+11 -1
View File
@@ -291,7 +291,17 @@ export function connectChatRun(): Socket {
const baseUrl = getBaseUrlValue()
const token = getApiKey()
const profile = localStorage.getItem('hermes_active_profile_name') || 'default'
// Get active profile from store (authoritative source)
let profile = 'default'
try {
const { useProfilesStore } = require('@/stores/hermes/profiles')
const profilesStore = useProfilesStore()
profile = profilesStore.activeProfileName || 'default'
} catch {
// Fallback to localStorage during early initialization
profile = localStorage.getItem('hermes_active_profile_name') || 'default'
}
chatRunSocket = io(`${baseUrl}/chat-run`, {
auth: { token },
+8
View File
@@ -5,6 +5,14 @@ export interface ChangelogEntry {
}
export const changelog: ChangelogEntry[] = [
{
version: '0.5.9',
date: '2026-05-04',
changes: [
'changelog.new_0_5_9_1',
'changelog.new_0_5_9_2',
],
},
{
version: '0.5.8',
date: '2026-05-03',
+2
View File
@@ -609,6 +609,8 @@ jobTriggered: 'Job ausgelost',
new_0_5_6_6: 'Attachment-Verarbeitung neu gestaltet mit Anthropic-Stil ContentBlock-Array-Format (Text, Bild, Datei)',
new_0_5_6_7: 'Frontend-Dateidownload-Funktion für ContentBlock- und Markdown-Formate mit Authentifizierung hinzugefügt',
new_0_5_6_8: 'Multi-Prozess-Konflikt behoben, der SQLite-Database-Resets verursacht hat, durch Entfernen redundanter nodemon-Instanzen',
new_0_5_9_1: 'Profilverwaltung在整个应用程序中统一,mit konsistentem API und State-Management',
new_0_5_9_2: 'GitHub-Issue- und Pull-Request-Vorlagen für besseren Contribution-Workflow hinzugefügt',
new_0_5_8_1: 'Drawer-Panel mit Mobile-Sidebar-Support und anpassbarem Rainbow-Button hinzugefügt',
new_0_5_8_2: 'Profile-Switching-State-Sync-Problem behoben mit sofortiger UI-Aktualisierung und Backend-Verifizierung',
new_0_5_8_3: 'Sonderzeichen und Emoji in der Sprachwiedergabe gefiltert für bessere Text-to-Speech',
+2
View File
@@ -780,6 +780,8 @@ export default {
new_0_5_6_6: 'Redesigned attachment handling using Anthropic-style ContentBlock array format with type discriminated unions (text, image, file)',
new_0_5_6_7: 'Added frontend file download functionality supporting both ContentBlock and Markdown formats with authentication',
new_0_5_6_8: 'Fixed multi-process conflict causing SQLite database resets by eliminating redundant nodemon instances',
new_0_5_9_1: 'Unify profile management across the application with consistent API and state management',
new_0_5_9_2: 'Add GitHub issue and pull request templates for better contribution workflow',
new_0_5_8_1: 'Add drawer panel with mobile sidebar support and customizable rainbow button',
new_0_5_8_2: 'Fix profile switching state sync issue with immediate UI update and backend verification',
new_0_5_8_3: 'Filter special characters and emoji in speech playback for better text-to-speech',
+2
View File
@@ -605,6 +605,8 @@ jobTriggered: 'Job ejecutado',
new_0_5_6_6: 'Rediseñado el manejo de adjuntos usando formato de matriz ContentBlock estilo Anthropic (texto, imagen, archivo)',
new_0_5_6_7: 'Añadida funcionalidad de descarga de archivos en frontend soportando formatos ContentBlock y Markdown con autenticación',
new_0_5_6_8: 'Corregido conflicto de múltiples procesos que causaba reinicios de base de datos SQLite eliminando instancias nodemon redundantes',
new_0_5_9_1: 'Unificar la gestión de perfiles en toda la aplicación con API y gestión de estado consistentes',
new_0_5_9_2: 'Añadir plantillas de issues y pull requests de GitHub para un mejor flujo de contribución',
new_0_5_8_1: 'Añadir panel de cajón con soporte de barra lateral móvil y botón arcoíris personalizable',
new_0_5_8_2: 'Corregir problema de sincronización de estado de cambio de perfil con actualización inmediata de UI y verificación de backend',
new_0_5_8_3: 'Filtrar caracteres especiales y emoji en reproducción de voz para mejor síntesis de voz',
+2
View File
@@ -605,6 +605,8 @@ jobTriggered: 'Job declenche',
new_0_5_6_6: 'Repensé la gestion des pièces jointes en utilisant le format de tableau ContentBlock style Anthropic (texte, image, fichier)',
new_0_5_6_7: 'Ajouté la fonctionnalité de téléchargement de fichiers frontend supportant les formats ContentBlock et Markdown avec authentification',
new_0_5_6_8: 'Corrigé le conflit multi-processus causant des réinitialisations de base de données SQLite en éliminant les instances nodemon redondantes',
new_0_5_9_1: 'Unifier la gestion des profils dans toute l\'application avec une API et une gestion d\'état cohérentes',
new_0_5_9_2: 'Ajouter des modèles issues et pull requests GitHub pour un meilleur flux de contribution',
new_0_5_8_1: 'Ajouter le panneau de tiroir avec support de barre latérale mobile et bouton arc-en-ciel personnalisable',
new_0_5_8_2: 'Corriger le problème de synchronisation d\'état de changement de profil avec mise à jour immédiate de l\'UI et vérification du backend',
new_0_5_8_3: 'Filtrer les caractères spéciaux et emoji dans la lecture vocale pour une meilleure synthèse vocale',
+2
View File
@@ -605,6 +605,8 @@ export default {
new_0_5_6_6: 'AnthropicスタイルのContentBlock配列形式(テキスト、画像、ファイル)を使用して添付ファイル処理を再設計',
new_0_5_6_7: 'ContentBlockおよびMarkdown形式をサポートし、認証付きのフロントエンドファイルダウンロード機能を追加',
new_0_5_6_8: '重複するnodemonインスタンスを削除し、SQLiteデータベースのリセットを引き起こすマルチプロセス競合を修正',
new_0_5_9_1: 'アプリケーション全体でプロファイル管理を統一し、一貫したAPIと状態管理を提供',
new_0_5_9_2: 'より良いコントリビューションワークフローのためにGitHubイシューとプルリクエストテンプレートを追加',
new_0_5_8_1: 'モバイルサイドバーサポートとカスタマイズ可能なレインボーボタン付きドロワーパネルを追加',
new_0_5_8_2: 'プロファイル切り替え状態同期問題を修正し、UIを即座に更新してバックエンドを検証',
new_0_5_8_3: '音声合成を改善するため、音声再生の特殊文字と絵文字をフィルタリング',
+2
View File
@@ -605,6 +605,8 @@ export default {
new_0_5_6_6: 'Anthropic 스타일의 ContentBlock 배열 형식(텍스트, 이미지, 파일)을 사용하여 첨부파일 처리를 재설계',
new_0_5_6_7: '인증이 포함된 ContentBlock 및 Markdown 형식을 지원하는 프론트엔드 파일 다운로드 기능 추가',
new_0_5_6_8: '중복된 nodemon 인스턴스를 제거하여 SQLite 데이터베이스 재설정을 일으키는 다중 프로세스 충돌 수정',
new_0_5_9_1: '일관된 API 및 상태 관리로 전체 응용 프로그램에서 프로필 관리 통합',
new_0_5_9_2: '더 나은 기여 워크플로우를 위해 GitHub 이슈 및 풀 리퀘스트 템플릿 추가',
new_0_5_8_1: '모바일 사이드바 지원 및 사용자 정의 가능한 무지개 버튼이 포함된 서랍 패널 추가',
new_0_5_8_2: '프로필 전환 상태 동기화 문제를 수정하고 즉시 UI 업데이트 및 백엔드 검증',
new_0_5_8_3: '음성 합성 개선을 위해 음성 재생의 특수 문자 및 이모지 필터링',
+2
View File
@@ -605,6 +605,8 @@ jobTriggered: 'Job acionado',
new_0_5_6_6: 'Processamento de anexos reprojetado usando formato de matriz ContentBlock estilo Anthropic (texto, imagem, arquivo)',
new_0_5_6_7: 'Adicionada funcionalidade de download de arquivos frontend suportando formatos ContentBlock e Markdown com autenticação',
new_0_5_6_8: 'Corrigido conflito de múltiplos processos que causava redefinições do banco de dados SQLite eliminando instâncias nodemon redundantes',
new_0_5_9_1: 'Unificar gerenciamento de perfis em todo o aplicativo com API e gerenciamento de estado consistentes',
new_0_5_9_2: 'Adicionar modelos de issues e pull requests do GitHub para melhor fluxo de contribuição',
new_0_5_8_1: 'Adicionar painel de gaveta com suporte de barra lateral móvel e botão arco-íris personalizável',
new_0_5_8_2: 'Corrigir problema de sincronização de estado de troca de perfil com atualização imediata de IU e verificação de backend',
new_0_5_8_3: 'Filtrar caracteres especiais e emoji na reprodução de voz para melhor síntese de voz',
+2
View File
@@ -782,6 +782,8 @@ export default {
new_0_5_6_6: '重新设计附件处理,采用 Anthropic 风格的 ContentBlock 数组格式,支持类型区分(文本、图片、文件)',
new_0_5_6_7: '新增前端文件下载功能,支持 ContentBlock 和 Markdown 两种格式,带身份验证',
new_0_5_6_8: '修复多进程冲突导致的 SQLite 数据库重置问题,清理冗余 nodemon 进程',
new_0_5_9_1: '统一应用程序中的 profile 管理,提供一致的 API 和状态管理',
new_0_5_9_2: '添加 GitHub issue 和 pull request 模板以改进贡献工作流程',
new_0_5_8_1: '新增抽屉面板支持移动端侧边栏,可自定义彩虹边框按钮',
new_0_5_8_2: '修复 profile 切换状态同步问题,立即更新 UI 并验证后端状态',
new_0_5_8_3: '过滤语音播放中的特殊字符和表情符号,改善语音合成效果',
@@ -7,8 +7,9 @@ const HUMAN_ONLY_KEY_PREFIX = 'hermes_human_only_v1_'
function currentProfileName(): string {
try {
return useProfilesStore().activeProfileName || localStorage.getItem('hermes_active_profile_name') || 'default'
return useProfilesStore().activeProfileName || 'default'
} catch {
// Fallback during store initialization
return localStorage.getItem('hermes_active_profile_name') || 'default'
}
}
@@ -3,9 +3,15 @@ import { readdir, stat, readFile } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
import { existsSync } from 'fs'
import { getActiveProfileDir } from '../../services/hermes/hermes-profile'
const HERMES_BASE = join(homedir(), '.hermes')
const CRON_OUTPUT = join(HERMES_BASE, 'cron', 'output')
function getCronOutputDir(): string {
// Use the active profile's directory, so cron history follows profile switches
const profileDir = getActiveProfileDir()
return join(profileDir, 'cron', 'output')
}
export interface RunEntry {
jobId: string
@@ -24,20 +30,21 @@ export interface RunDetail {
/** List all run output files, optionally filtered by job ID */
export async function listRuns(ctx: Context) {
const jobId = ctx.query.jobId as string | undefined
const cronOutput = getCronOutputDir()
if (!existsSync(CRON_OUTPUT)) {
if (!existsSync(cronOutput)) {
ctx.body = { runs: [] }
return
}
try {
const dirs = await readdir(CRON_OUTPUT)
const dirs = await readdir(cronOutput)
const runs: RunEntry[] = []
const targetDirs = jobId ? dirs.filter(d => d === jobId) : dirs
for (const dir of targetDirs) {
const dirPath = join(CRON_OUTPUT, dir)
const dirPath = join(cronOutput, dir)
try {
const dirStat = await stat(dirPath)
if (!dirStat.isDirectory()) continue
@@ -93,7 +100,8 @@ export async function readRun(ctx: Context) {
return
}
const filePath = join(CRON_OUTPUT, jobId, fileName)
const cronOutput = getCronOutputDir()
const filePath = join(cronOutput, jobId, fileName)
if (!existsSync(filePath)) {
ctx.status = 404
+14 -1
View File
@@ -13,7 +13,20 @@ function getApiKey(profile: string): string | null {
}
function resolveProfile(ctx: Context): string {
return ctx.get('x-hermes-profile') || (ctx.query.profile as string) || 'default'
// Use header/query from request first, then fall back to authoritative source
const requestedProfile = ctx.get('x-hermes-profile') || (ctx.query.profile as string)
if (requestedProfile) {
return requestedProfile
}
// Fallback: read from authoritative source (active_profile file)
try {
const { getActiveProfileName } = require('../../services/hermes/hermes-profile')
return getActiveProfileName()
} catch {
return 'default'
}
}
function buildHeaders(profile: string): Record<string, string> {
@@ -11,6 +11,24 @@ import { smartCloneCleanup } from '../../services/hermes/profile-credentials'
export async function list(ctx: any) {
try {
const profiles = await hermesCli.listProfiles()
// Override active flag from the authoritative source (active_profile file)
// CLI output may be stale, but the file is written by hermes profile use
const { getActiveProfileName } = await import('../../services/hermes/hermes-profile')
const activeProfileName = getActiveProfileName()
// Check if CLI's active flag matches the file (warn if inconsistent)
const cliActive = profiles.find(p => p.active)
if (cliActive?.name !== activeProfileName) {
logger.warn('[listProfiles] CLI active flag (%s) differs from active_profile file (%s) - using file as authoritative source',
cliActive?.name || 'none', activeProfileName)
}
// Fix the active flag based on the actual active_profile file
profiles.forEach(p => {
p.active = (p.name === activeProfileName)
})
ctx.body = { profiles }
} catch (err: any) {
ctx.status = 500
@@ -142,9 +160,31 @@ export async function switchProfile(ctx: any) {
}
try {
const output = await hermesCli.useProfile(name)
await new Promise(r => setTimeout(r, 1000))
// Verify the active_profile file immediately (Hermes CLI writes synchronously)
// Quick verification with 2 retries to handle edge cases (filesystem delays, concurrency)
const { getActiveProfileName } = await import('../../services/hermes/hermes-profile')
let actualActive = getActiveProfileName()
// Quick retry (max 2 times, 100ms delay each)
for (let i = 0; i < 2; i++) {
if (actualActive === name) break
logger.debug('[switchProfile] Quick retry %d: current=%s, expected=%s', i + 1, actualActive, name)
await new Promise(r => setTimeout(r, 100))
actualActive = getActiveProfileName()
}
if (actualActive !== name) {
logger.error('[switchProfile] Verification failed: active_profile is %s (expected %s)', actualActive, name)
ctx.status = 500
ctx.body = { error: `Profile switch verification failed - active profile is ${actualActive}` }
return
}
// Update GatewayManager to match the authoritative source
const mgr = getGatewayManagerInstance()
if (mgr) { mgr.setActiveProfile(name) }
try {
const detail = await hermesCli.getProfile(name)
logger.debug('Profile detail.path = %s', detail.path)
@@ -159,12 +199,14 @@ export async function switchProfile(ctx: any) {
} catch (err: any) {
logger.error(err, 'Ensure config failed')
}
const drainResult = await SessionDeleter.getInstance().drain(name)
SessionDeleter.getInstance().switchProfile(name)
logger.info('[switchProfile] drain result for profile "%s": %d deleted, %d failed', name, drainResult.deleted.length, drainResult.failed.length)
if (drainResult.failed.length > 0) {
logger.warn({ profile: name, failed: drainResult.failed }, 'Failed to drain some pending session deletes after profile switch')
}
ctx.body = {
success: true,
message: output.trim(),
@@ -49,7 +49,20 @@ async function waitForGatewayReady(upstream: string, timeoutMs: number = 5000):
/** Resolve profile name from request */
function resolveProfile(ctx: Context): string {
return ctx.get('x-hermes-profile') || (ctx.query.profile as string) || 'default'
// Use header/query from request, but fall back to authoritative source if not provided
const requestedProfile = ctx.get('x-hermes-profile') || (ctx.query.profile as string)
if (requestedProfile) {
return requestedProfile
}
// Fallback: read from authoritative source (active_profile file)
try {
const { getActiveProfileName } = require('../../services/hermes/hermes-profile')
return getActiveProfileName()
} catch {
return 'default'
}
}
/** Resolve upstream URL for a request based on profile header/query */