[codex] fix auth startup and profile model defaults (#966)

* docs release 0.6.0 changelog

* fix auth startup and profile model defaults
This commit is contained in:
ekko
2026-05-24 14:00:31 +08:00
committed by GitHub
parent 634a622934
commit f61a1d9454
24 changed files with 310 additions and 30 deletions
+16 -1
View File
@@ -136,6 +136,21 @@ Unified configuration for **8 platforms** in one page:
- Super administrators can manage users and profile bindings; regular administrators can manage their own account details
- Auth can be disabled with `AUTH_DISABLED=1`
CLI maintenance commands:
```bash
# Delete persisted login IP lock records
hermes-web-ui clear-login-locks
# Delete login locks and restart the running Web UI process
hermes-web-ui clear-login-locks --restart
# Create or reset the default super administrator login to admin / 123456
hermes-web-ui reset-default-login
```
`clear-login-locks` removes `${HERMES_WEB_UI_HOME:-~/.hermes-web-ui}/.login-lock.json`. If the server is running, restart it to clear in-memory lock state. `reset-default-login` updates the Web UI account database; if an `admin` user already exists, its password is reset to `123456` and the account is enabled as a super administrator.
### Settings
- Display (streaming, compact mode, reasoning, cost display)
@@ -293,7 +308,7 @@ Browser → BFF (Koa, :8648) → Socket.IO /chat-run
The frontend is designed with **multi-agent extensibility** — all Hermes-specific code is namespaced under `hermes/` directories (API, components, views, stores), making it straightforward to add new agent integrations alongside.
The BFF layer handles Socket.IO chat streaming, the Hermes agent bridge, profile-aware file upload and path-based download (multi-backend: local/Docker/SSH/Singularity), session CRUD, account/profile authorization, config/credential management, WeChat QR login, model discovery, skills/memory management, log reading, and static file serving.
The BFF layer handles Socket.IO chat streaming, the Hermes agent bridge, profile-aware file upload and path-based download (multi-backend: local/Docker/SSH/Singularity), session CRUD, account- and profile-scoped management, config/credential management, WeChat QR login, model discovery, skills/memory management, log reading, and static file serving.
## Tech Stack
+16 -1
View File
@@ -144,6 +144,21 @@
- 超级管理员可以管理用户和 Profile 绑定;普通管理员只能管理自己的账户信息
- 可通过 `AUTH_DISABLED=1` 禁用认证
CLI 维护命令:
```bash
# 删除持久化的登录 IP 锁记录
hermes-web-ui clear-login-locks
# 删除登录锁并重启正在运行的 Web UI 进程
hermes-web-ui clear-login-locks --restart
# 创建或重置默认超级管理员登录名/密码为 admin / 123456
hermes-web-ui reset-default-login
```
`clear-login-locks` 会删除 `${HERMES_WEB_UI_HOME:-~/.hermes-web-ui}/.login-lock.json`。如果服务正在运行,需要重启服务才能清理内存中的锁定状态。`reset-default-login` 会更新 Web UI 账户数据库;如果已存在 `admin` 用户,则会把密码重置为 `123456`,并启用为超级管理员账户。
### 设置
- 显示(流式输出、紧凑模式、推理过程、费用显示)
@@ -300,7 +315,7 @@ npm run build # 构建输出到 dist/
前端采用 **多 Agent 可扩展架构** — 所有 Hermes 相关代码都按命名空间组织在 `hermes/` 目录下(API、组件、视图、Store),可以方便地并行接入新的 Agent。
BFF 层负责:Socket.IO 聊天流式推送、Hermes agent bridge、按 Profile 隔离的上传和按路径解析的下载(多 Backend 支持:local/Docker/SSH/Singularity)、会话 CRUD、账号/Profile 鉴权、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。
BFF 层负责:Socket.IO 聊天流式推送、Hermes agent bridge、按 Profile 隔离的上传和按路径解析的下载(多 Backend 支持:local/Docker/SSH/Singularity)、会话 CRUD、分账户分 Profile 管理、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。
## 技术栈
+3 -9
View File
@@ -390,9 +390,7 @@ function startDaemon(port) {
fetch(healthUrl).then(res => {
if (res.ok) {
const url = token
? `http://localhost:${port}/#/?token=${token}`
: `http://localhost:${port}`
const url = `http://localhost:${port}`
console.log(` ✓ hermes-web-ui started`)
console.log(` ${url}`)
console.log(` Log: ${LOG_FILE}`)
@@ -404,9 +402,7 @@ function startDaemon(port) {
} else {
console.log(` ⚠ Server process is running but health check failed after ${maxWait / 1000}s`)
console.log(` Check log: ${LOG_FILE}`)
const url = token
? `http://localhost:${port}/#/?token=${token}`
: `http://localhost:${port}`
const url = `http://localhost:${port}`
console.log(` ${url}`)
}
}).catch(() => {
@@ -415,9 +411,7 @@ function startDaemon(port) {
} else {
console.log(` ⚠ Server process is running but health check failed after ${maxWait / 1000}s`)
console.log(` Check log: ${LOG_FILE}`)
const url = token
? `http://localhost:${port}/#/?token=${token}`
: `http://localhost:${port}`
const url = `http://localhost:${port}`
console.log(` ${url}`)
}
})
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-web-ui",
"version": "0.5.35",
"version": "0.6.0",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": {
"type": "git",
@@ -123,4 +123,4 @@
"vue-tsc": "^3.2.8",
"ws": "^8.20.0"
}
}
}
+14
View File
@@ -5,6 +5,20 @@ export interface ChangelogEntry {
}
export const changelog: ChangelogEntry[] = [
{
version: '0.6.0',
date: '2026-05-24',
changes: [
'changelog.new_0_6_0_1',
'changelog.new_0_6_0_2',
'changelog.new_0_6_0_3',
'changelog.new_0_6_0_4',
'changelog.new_0_6_0_5',
'changelog.new_0_6_0_6',
'changelog.new_0_6_0_7',
'changelog.new_0_6_0_8',
],
},
{
version: '0.5.35',
date: '2026-05-23',
+8
View File
@@ -1119,6 +1119,14 @@ jobTriggered: 'Job ausgelost',
new_0_5_35_6: 'Performance Monitoring blockiert nicht mehr auf worker-Anfragen während Agent-Initialisierung und reduziert request timeouts unter Windows',
new_0_5_35_7: 'Chat-Markdown unterstützt Inline-Vorschauen für Textinhalte, und Download-Icons laden Dateien direkt herunter statt die Vorschau zu öffnen',
new_0_5_35_8: 'Content-Preview-Drawer verbessert: Schließen-Aktion auf Mobilgeräten, mobile Vollbreite, 800px Desktop-Breite und konsistente Text-/Markdown-Hintergründe',
new_0_6_0_1: 'Account- und Profile-bezogene Verwaltung schützt Sessions, Modelle, Nutzung, Kanban, Jobs, Uploads, Medien und verwandte Hermes APIs konsistent',
new_0_6_0_2: 'Gebündelte Medien-Skills verwenden das generierte Server-Token nur für Medien-Endpunkte und lösen fun-codex/xAI-Zugangsdaten aus dem angeforderten Profile auf',
new_0_6_0_3: 'Einzelchat und Gruppenchat injizieren das aktuelle Hermes Profile in Run-Instructions, damit Skills X-Hermes-Profile senden können',
new_0_6_0_4: 'delegate_task-Subagent-Fortschritt wird mit Start-, Tool-, Fortschritts- und Abschlussstatus in die Chat-UI gestreamt',
new_0_6_0_5: 'Stoppen oder Abbrechen eines Runs bereinigt temporäre Events, damit alte abort-Zustände nicht in den nächsten Chat gelangen',
new_0_6_0_6: 'Dokumentation und Website-Texte für Accountverwaltung, Standard-Zugangsdaten, Account/Profile-Verwaltung, Uploads/Downloads und Medien-Skills aktualisiert',
new_0_6_0_7: 'CLI-Wartungsbefehle zum Löschen von Login-IP-Sperren und Zurücksetzen des Standard-Logins admin / 123456 hinzugefügt',
new_0_6_0_8: 'Version 0.6.0 ist die Grenze zwischen Single-User- und Multi-User-Web-UI. Bei Problemen im Multi-User-Modus bitte ein Issue erstellen und bei Bedarf auf die Single-User-Version 0.5.35 zurückgehen',
},
// Dateien
+8
View File
@@ -1341,6 +1341,14 @@ export default {
new_0_5_35_6: 'Performance monitoring no longer blocks on worker requests while agents are initializing, reducing request timeouts on Windows',
new_0_5_35_7: 'Chat Markdown now supports inline text-content previews, and download icons download files directly instead of opening the preview drawer',
new_0_5_35_8: 'Polish the content preview drawer with a mobile close action, full-width mobile layout, 800px desktop width, and consistent text/Markdown backgrounds',
new_0_6_0_1: 'Account- and Profile-scoped management now consistently protects sessions, models, usage, Kanban, jobs, uploads, media, and related Hermes APIs',
new_0_6_0_2: 'Bundled media skills use the generated server token only for media endpoints and resolve fun-codex/xAI credentials from the requested Profile',
new_0_6_0_3: 'Single chat and group chat now inject the current Hermes Profile into run instructions so skills can send X-Hermes-Profile',
new_0_6_0_4: 'delegate_task subagent progress now streams into the chat UI with start, tool, progress, and completion updates',
new_0_6_0_5: 'Stopping or aborting a run clears transient events so stale abort state does not leak into the next chat',
new_0_6_0_6: 'Update docs and website copy for account management, default credentials, account/Profile management, uploads/downloads, and bundled media skills',
new_0_6_0_7: 'Add CLI maintenance commands for clearing login IP locks and resetting the default admin / 123456 login',
new_0_6_0_8: 'Version 0.6.0 is the boundary between single-user and multi-user Web UI. If multi-user mode causes issues, please file an issue and roll back to the 0.5.35 single-user release if needed',
},
}
+8
View File
@@ -1119,6 +1119,14 @@ jobTriggered: 'Job ejecutado',
new_0_5_35_6: 'Performance Monitor ya no se bloquea con requests a workers durante la inicialización de Agent, reduciendo request timeouts en Windows',
new_0_5_35_7: 'Chat Markdown ahora soporta vista previa inline de contenido de texto, y los iconos de descarga bajan archivos directamente sin abrir el drawer',
new_0_5_35_8: 'Mejora el drawer de contenido con cierre en móvil, ancho completo en móvil, 800px en escritorio y fondos consistentes para texto/Markdown',
new_0_6_0_1: 'La gestión por cuenta y por Profile protege de forma coherente sesiones, modelos, uso, Kanban, jobs, subidas, medios y APIs Hermes relacionadas',
new_0_6_0_2: 'Los Skills de medios integrados usan el token de servidor generado solo para endpoints de medios y resuelven credenciales fun-codex/xAI desde el Profile solicitado',
new_0_6_0_3: 'El chat individual y el grupal inyectan el Hermes Profile actual en las instrucciones del run para que los Skills envíen X-Hermes-Profile',
new_0_6_0_4: 'El progreso de subagents de delegate_task se muestra en la UI del chat con estados de inicio, herramienta, progreso y finalización',
new_0_6_0_5: 'Al detener o abortar un run se limpian eventos temporales para que el estado abort anterior no pase al siguiente chat',
new_0_6_0_6: 'Actualiza documentación y sitio web para gestión de cuentas, credenciales predeterminadas, gestión cuenta/Profile, subidas/descargas y Skills de medios',
new_0_6_0_7: 'Añade comandos CLI de mantenimiento para limpiar bloqueos de IP de login y restablecer el login predeterminado admin / 123456',
new_0_6_0_8: 'La versión 0.6.0 marca el límite entre la Web UI de usuario único y multiusuario. Si el modo multiusuario causa problemas, abre un issue y vuelve a la versión 0.5.35 de usuario único si es necesario',
},
// Archivos
+8
View File
@@ -1119,6 +1119,14 @@ jobTriggered: 'Job declenche',
new_0_5_35_6: 'Performance Monitor ne bloque plus sur les requêtes worker pendant linitialisation des Agents, réduisant les request timeouts sous Windows',
new_0_5_35_7: 'Chat Markdown prend désormais en charge la prévisualisation inline du contenu texte, et les icônes de téléchargement téléchargent directement les fichiers',
new_0_5_35_8: 'Améliore le drawer de contenu avec fermeture mobile, largeur complète sur mobile, largeur desktop 800px et fonds texte/Markdown cohérents',
new_0_6_0_1: 'La gestion par compte et par Profile protège désormais de façon cohérente les sessions, modèles, usage, Kanban, jobs, uploads, médias et APIs Hermes associées',
new_0_6_0_2: 'Les Skills média intégrés utilisent le token serveur généré uniquement pour les endpoints média et résolvent les identifiants fun-codex/xAI depuis le Profile demandé',
new_0_6_0_3: 'Le chat individuel et le chat de groupe injectent le Hermes Profile courant dans les instructions de run afin que les Skills envoient X-Hermes-Profile',
new_0_6_0_4: 'La progression des subagents delegate_task est maintenant diffusée dans lUI du chat avec les états démarrage, outil, progression et fin',
new_0_6_0_5: 'Larrêt ou labandon dun run nettoie les événements temporaires afin que les anciens états abort ne passent pas au chat suivant',
new_0_6_0_6: 'Met à jour la documentation et le site pour la gestion des comptes, les identifiants par défaut, la gestion compte/Profile, les uploads/downloads et les Skills média',
new_0_6_0_7: 'Ajoute des commandes CLI de maintenance pour effacer les verrous IP de connexion et réinitialiser le login par défaut admin / 123456',
new_0_6_0_8: 'La version 0.6.0 marque la limite entre la Web UI mono-utilisateur et multi-utilisateur. En cas de problème avec le mode multi-utilisateur, ouvrez une issue et revenez si besoin à la version mono-utilisateur 0.5.35',
},
// Fichiers
+8
View File
@@ -1119,6 +1119,14 @@ export default {
new_0_5_35_6: 'Agent 初期化中の worker request で Performance Monitor がブロックされにくくなり、Windows の request timed out を軽減',
new_0_5_35_7: 'Chat Markdown にテキスト内容のインラインプレビューを追加し、download アイコンはプレビュー drawer を開かず直接ダウンロードします',
new_0_5_35_8: '内容表示 drawer を改善し、モバイルの閉じる操作と全幅表示、デスクトップ 800px 幅、テキスト/Markdown 背景の統一に対応',
new_0_6_0_1: 'アカウント別・Profile 別の管理により、セッション、モデル、使用量、Kanban、ジョブ、アップロード、メディア、関連 Hermes API を一貫して保護します',
new_0_6_0_2: '内蔵メディア Skills は生成されたサーバートークンをメディア endpoint のみに使用し、要求された Profile から fun-codex/xAI 認証情報を解決します',
new_0_6_0_3: '単一チャットとグループチャットは現在の Hermes Profile を run instructions に注入し、Skills が X-Hermes-Profile を送れるようにします',
new_0_6_0_4: 'delegate_task の subagent 進行状況をチャット UI に stream 表示し、開始、tool、進行、完了を確認できます',
new_0_6_0_5: '停止または中断時に一時イベントをクリアし、古い abort 状態が次のチャットに漏れないようにしました',
new_0_6_0_6: 'アカウント管理、既定の認証情報、アカウント/Profile 管理、アップロード/ダウンロード、内蔵メディア Skills のドキュメントとサイト文言を更新',
new_0_6_0_7: 'ログイン IP ロックのクリアと既定の admin / 123456 ログインをリセットする CLI メンテナンスコマンドを追加',
new_0_6_0_8: '0.6.0 は Web UI の単一ユーザー版とマルチユーザー版の境界です。マルチユーザー機能に問題がある場合は issue を送信し、必要に応じて 0.5.35 の単一ユーザー版へ戻してください',
},
// ファイル
+8
View File
@@ -1119,6 +1119,14 @@ export default {
new_0_5_35_6: 'Agent 초기화 중 worker request로 Performance Monitor가 막히지 않도록 개선해 Windows의 request timed out 가능성을 낮춤',
new_0_5_35_7: 'Chat Markdown에 텍스트 콘텐츠 인라인 미리보기를 추가하고, 다운로드 아이콘은 preview drawer 대신 파일을 직접 다운로드합니다',
new_0_5_35_8: '콘텐츠 preview drawer 개선: 모바일 닫기 동작, 모바일 전체 너비, 데스크톱 800px 너비, 텍스트/Markdown 배경 통일',
new_0_6_0_1: '계정별 및 Profile별 관리가 세션, 모델, 사용량, Kanban, 작업, 업로드, 미디어, 관련 Hermes API를 일관되게 보호합니다',
new_0_6_0_2: '내장 미디어 Skills는 생성된 서버 토큰을 미디어 엔드포인트에만 사용하고 요청한 Profile에서 fun-codex/xAI 자격 증명을 확인합니다',
new_0_6_0_3: '단일 채팅과 그룹 채팅이 현재 Hermes Profile을 run instructions에 주입해 Skills가 X-Hermes-Profile을 보낼 수 있습니다',
new_0_6_0_4: 'delegate_task subagent 진행 상황이 시작, 도구, 진행, 완료 상태로 채팅 UI에 스트리밍됩니다',
new_0_6_0_5: '실행 중지 또는 중단 시 임시 이벤트를 정리해 이전 abort 상태가 다음 채팅으로 넘어가지 않게 했습니다',
new_0_6_0_6: '계정 관리, 기본 자격 증명, 계정/Profile 관리, 업로드/다운로드, 내장 미디어 Skills 문서와 웹사이트 문구를 업데이트했습니다',
new_0_6_0_7: '로그인 IP 잠금을 지우고 기본 admin / 123456 로그인을 재설정하는 CLI 유지보수 명령을 추가했습니다',
new_0_6_0_8: '0.6.0은 Web UI의 단일 사용자 버전과 다중 사용자 버전의 경계입니다. 다중 사용자 모드에 문제가 있으면 issue를 제출하고, 필요하면 0.5.35 단일 사용자 버전으로 되돌리세요',
},
// 파일
+8
View File
@@ -1119,6 +1119,14 @@ jobTriggered: 'Job acionado',
new_0_5_35_6: 'Performance Monitor não bloqueia mais em requests a workers durante a inicialização de Agents, reduzindo request timeouts no Windows',
new_0_5_35_7: 'Chat Markdown agora suporta preview inline de conteúdo de texto, e ícones de download baixam arquivos diretamente sem abrir o drawer',
new_0_5_35_8: 'Melhora o drawer de conteúdo com ação de fechar no mobile, largura total no mobile, 800px no desktop e fundos consistentes para texto/Markdown',
new_0_6_0_1: 'A gestão por conta e por Profile agora protege de forma consistente sessões, modelos, uso, Kanban, jobs, uploads, mídia e APIs Hermes relacionadas',
new_0_6_0_2: 'Skills de mídia integrados usam o token de servidor gerado apenas para endpoints de mídia e resolvem credenciais fun-codex/xAI a partir do Profile solicitado',
new_0_6_0_3: 'Chat individual e em grupo injetam o Hermes Profile atual nas instruções do run para que Skills enviem X-Hermes-Profile',
new_0_6_0_4: 'O progresso de subagents de delegate_task aparece na UI do chat com estados de início, ferramenta, progresso e conclusão',
new_0_6_0_5: 'Parar ou abortar um run limpa eventos temporários para que estados abort antigos não passem para o próximo chat',
new_0_6_0_6: 'Atualiza documentação e site para gestão de contas, credenciais padrão, gestão conta/Profile, uploads/downloads e Skills de mídia',
new_0_6_0_7: 'Adiciona comandos CLI de manutenção para limpar bloqueios de IP de login e redefinir o login padrão admin / 123456',
new_0_6_0_8: 'A versão 0.6.0 é o limite entre a Web UI de usuário único e multiusuário. Se o modo multiusuário causar problemas, abra uma issue e volte para a versão 0.5.35 de usuário único se necessário',
},
// Arquivos
@@ -1346,5 +1346,13 @@ export default {
new_0_5_35_6: '效能監控不再因 Agent 初始化中的 worker 請求而阻塞,降低 Windows 上 request timed out 的機率',
new_0_5_35_7: '聊天 Markdown 新增文字內容內嵌預覽,下載圖示會直接下載檔案,避免被預覽抽屜攔截',
new_0_5_35_8: '最佳化內容展示抽屜:行動端全寬並提供關閉入口,桌面端加寬到 800px,文字與 Markdown 背景保持一致',
new_0_6_0_1: '分帳戶、分 Profile 管理現在一致保護工作階段、模型、用量、看板、任務、上傳、媒體與相關 Hermes API',
new_0_6_0_2: '內建媒體 Skills 只會在媒體端點使用產生的伺服器 token,並依請求的 Profile 讀取 fun-codex/xAI 憑證',
new_0_6_0_3: '單聊與群聊都會把目前 Hermes Profile 注入執行提示,方便 Skills 請求時帶上 X-Hermes-Profile',
new_0_6_0_4: 'delegate_task 的 subagent 進度會即時顯示在聊天介面,包含開始、工具、進度與完成狀態',
new_0_6_0_5: '停止或中止執行時會清理暫時事件,避免舊的 abort 狀態帶入下一次聊天',
new_0_6_0_6: '同步更新文件與官網文案,涵蓋帳戶管理、預設憑證、分帳戶分 Profile 管理、上傳下載與內建媒體 Skills',
new_0_6_0_7: '新增 CLI 維護命令,用於清理登入 IP 鎖與重設預設 admin / 123456 登入帳戶',
new_0_6_0_8: '0.6.0 是 Web UI 從單使用者走向多使用者的分界版本;如果多使用者模式遇到問題,請提交 issue,必要時可回退到 0.5.35 單使用者版本',
},
}
+8
View File
@@ -1343,6 +1343,14 @@ export default {
new_0_5_35_6: '性能监控不再因为 Agent 初始化中的 worker 请求而阻塞,降低 Windows 上 request timed out 的概率',
new_0_5_35_7: '聊天 Markdown 新增文本内容内联预览,下载图标会直接下载文件,避免被预览弹窗拦截',
new_0_5_35_8: '优化内容展示抽屉:移动端全宽并提供关闭入口,桌面端加宽到 800px,文本与 Markdown 背景保持一致',
new_0_6_0_1: '分账户、分 Profile 管理现在统一覆盖会话、模型、用量、看板、任务、上传、媒体以及相关 Hermes API',
new_0_6_0_2: '内置媒体 Skills 仅在媒体接口使用生成的服务端 token,并按请求的 Profile 读取 fun-codex/xAI 凭据',
new_0_6_0_3: '单聊和群聊都会向运行提示词注入当前 Hermes Profile,方便 Skills 请求时带上 X-Hermes-Profile',
new_0_6_0_4: 'delegate_task 的 subagent 进度会实时展示到聊天界面,包含开始、工具调用、进度和完成状态',
new_0_6_0_5: '停止或中断运行时会清理临时事件,避免旧的 abort 状态带入下一次聊天',
new_0_6_0_6: '同步更新文档和官网文案,覆盖账户管理、默认凭据、分账户分 Profile 管理、上传下载和内置媒体 Skills',
new_0_6_0_7: '新增 CLI 维护命令,用于清理登录 IP 锁和重置默认 admin / 123456 登录账户',
new_0_6_0_8: '0.6.0 是 Web UI 从单用户走向多用户的分界版本;如果多用户模式遇到问题,请提交 issue,必要时可回退到 0.5.35 单用户版本',
},
}
+13 -6
View File
@@ -20,6 +20,7 @@ import { hasApiKey } from '@/api/client'
const WEB_UI_VERSION = __APP_VERSION__
const SIDEBAR_COLLAPSED_KEY = 'hermes_sidebar_collapsed'
const ACTIVE_PROFILE_STORAGE_KEY = 'hermes_active_profile_name'
const MODELS_CACHE_TTL_MS = 30000
export const useAppStore = defineStore('app', () => {
@@ -89,13 +90,19 @@ export const useAppStore = defineStore('app', () => {
modelVisibility.value = res.model_visibility || {}
customModels.value = res.custom_models || {}
const defaultModel = res.default || ''
const defaultProvider = res.default_provider || ''
const explicitGroup = res.groups.find(g => g.provider === defaultProvider && g.models.includes(defaultModel))
const inferredGroup = res.groups.find(g => g.models.includes(defaultModel))
const fallbackGroup = res.groups.find(g => g.models.length > 0)
const activeProfileName = localStorage.getItem(ACTIVE_PROFILE_STORAGE_KEY) || ''
const activeProfileModels = activeProfileName
? profileModelGroups.value.find(entry => entry.profile === activeProfileName)
: undefined
const defaultSource = activeProfileModels || res
const defaultGroups = defaultSource.groups || []
const defaultModel = defaultSource.default || ''
const defaultProvider = defaultSource.default_provider || ''
const explicitGroup = defaultGroups.find(g => g.provider === defaultProvider && g.models.includes(defaultModel))
const inferredGroup = defaultGroups.find(g => g.models.includes(defaultModel))
const fallbackGroup = defaultGroups.find(g => g.models.length > 0)
const providerGroup = defaultProvider ? res.groups.find(g => g.provider === defaultProvider) : undefined
const providerGroup = defaultProvider ? defaultGroups.find(g => g.provider === defaultProvider) : undefined
const allProvider = defaultProvider ? res.allProviders.find(g => g.provider === defaultProvider) : undefined
const providerCatalog = providerGroup?.available_models?.length
? providerGroup.available_models
+1 -1
View File
@@ -56,7 +56,7 @@ export async function currentUser(ctx: Context) {
created_at: user.created_at,
updated_at: user.updated_at,
last_login_at: user.last_login_at,
requiresCredentialChange: user.username === DEFAULT_USERNAME || verifyPassword(DEFAULT_PASSWORD, user.password_hash),
requiresCredentialChange: user.username === DEFAULT_USERNAME && verifyPassword(DEFAULT_PASSWORD, user.password_hash),
},
}
}
@@ -201,7 +201,15 @@ function requestedProfileName(ctx: any): string {
}
function requestScopedProfileName(ctx: any): string {
return ctx.state?.profile?.name || getActiveProfileName() || 'default'
const headerProfile = typeof ctx.get === 'function' ? ctx.get('x-hermes-profile') : ''
const queryProfile = typeof ctx.query?.profile === 'string' ? ctx.query.profile : ''
const bodyProfile = typeof ctx.request?.body?.profile === 'string' ? ctx.request.body.profile : ''
return ctx.state?.profile?.name ||
headerProfile.trim() ||
queryProfile.trim() ||
bodyProfile.trim() ||
getActiveProfileName() ||
'default'
}
function visibleProfileNamesForUser(ctx: any): string[] {
@@ -411,7 +419,7 @@ export async function getAvailable(ctx: any) {
const mergedGroups = mergeAvailableGroups(profileResults.flatMap(result => result.groups))
const groupsWithAliases = applyModelAliases(mergedGroups, modelAliases)
const visibleGroups = applyModelVisibility(groupsWithAliases, modelVisibility)
const activeProfile = getActiveProfileName()
const activeProfile = requestScopedProfileName(ctx)
const defaultProfile = profileResults.find(result => result.profile === activeProfile && (result.default || result.default_provider))
|| profileResults.find(result => result.default && result.default_provider)
|| profileResults.find(result => result.default)
@@ -83,6 +83,13 @@ async function allowServerTokenForMedia(ctx: Context, token: string): Promise<bo
return true
}
function isProtectedHttpPath(path: string): boolean {
const lowerPath = path.toLowerCase()
return lowerPath.startsWith('/api') ||
lowerPath.startsWith('/v1') ||
lowerPath.startsWith('/upload')
}
export function signUserJwt(user: Pick<UserRecord, 'id' | 'username' | 'role'>, secret: string, now = Date.now()): string {
const iat = Math.floor(now / 1000)
const payload: JwtPayload = {
@@ -154,6 +161,11 @@ export async function isAuthEnabled(): Promise<boolean> {
}
export async function requireUserJwt(ctx: Context, next: Next): Promise<void> {
if (!isProtectedHttpPath(ctx.path)) {
await next()
return
}
const secret = await getJwtSecret()
if (!secret) {
await next()
+1 -1
View File
@@ -232,7 +232,7 @@ export default {
intro: 'Hermes Web UI provides a local BFF API for the dashboard and Socket.IO endpoints for streaming chat.',
local: {
title: 'Local BFF Endpoints',
content: 'The Koa server handles session management, profile CRUD, account/profile authorization, config read/write, log access, skill listing, memory operations, and static assets.',
content: 'The Koa server handles session management, profile CRUD, account- and profile-scoped management, config read/write, log access, skill listing, memory operations, and static assets.',
},
proxy: {
title: 'Chat Streaming',
+1 -1
View File
@@ -232,7 +232,7 @@ export default {
intro: 'Hermes Web UI 提供本地 BFF API,并通过 Socket.IO 端点进行聊天流式通信。',
local: {
title: '本地 BFF 端点',
content: 'Koa 服务器处理会话管理、Profile CRUD、账号/Profile 鉴权、配置读写、日志访问、技能列表、记忆操作和静态资源。',
content: 'Koa 服务器处理会话管理、Profile CRUD、分账户分 Profile 管理、配置读写、日志访问、技能列表、记忆操作和静态资源。',
},
proxy: {
title: '聊天流式通信',
+48
View File
@@ -232,6 +232,54 @@ describe('App Store', () => {
expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')
})
it('selects the browser active profile default instead of the aggregate response default', async () => {
window.localStorage.setItem('hermes_active_profile_name', 'tester')
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: 'glm-5-turbo',
default_provider: 'custom:glm-coding-plan',
groups: [{
provider: 'custom:glm-coding-plan',
label: 'glm-coding-plan',
base_url: 'https://api.z.ai/api/anthropic',
models: ['glm-5-turbo', 'glm-5.1'],
api_key: '',
}],
allProviders: [],
profiles: [
{
profile: 'default',
default: 'glm-5-turbo',
default_provider: 'custom:glm-coding-plan',
groups: [{
provider: 'custom:glm-coding-plan',
label: 'glm-coding-plan',
base_url: 'https://api.z.ai/api/anthropic',
models: ['glm-5-turbo', 'glm-5.1'],
api_key: '',
}],
},
{
profile: 'tester',
default: 'claude-opus-4-6',
default_provider: 'custom:subrouter',
groups: [{
provider: 'custom:subrouter',
label: 'subrouter',
base_url: 'https://subrouter.ai/v1',
models: ['claude-opus-4-6', 'gpt-5.5'],
api_key: '',
}],
},
],
})
const store = useAppStore()
await store.loadModels()
expect(store.selectedModel).toBe('claude-opus-4-6')
expect(store.selectedProvider).toBe('custom:subrouter')
})
it('does not refetch available models within the cache window after an empty response', async () => {
mockSystemApi.fetchAvailableModels.mockResolvedValue({
default: '',
@@ -37,7 +37,15 @@ async function loadSkillsController() {
}
function makeCtx(body: unknown): any {
return { request: { body }, status: 200, body: undefined, query: {}, params: {} }
return {
request: { body },
status: 200,
body: undefined,
query: {},
params: {},
state: {},
get: vi.fn(() => ''),
}
}
beforeEach(async () => {
@@ -75,6 +83,24 @@ describe('config mutating controllers', () => {
expect(config.terminal.backend).toBe('local')
})
it('setConfigModel uses the requested profile header when auth has not populated state.profile', async () => {
const researchDir = join(hermesHome, 'profiles', 'research')
await mkdir(researchDir, { recursive: true })
await writeFile(join(hermesHome, 'config.yaml'), 'model:\n default: root-model\n', 'utf-8')
await writeFile(join(researchDir, 'config.yaml'), 'model:\n default: old-research\n', 'utf-8')
const { setConfigModel } = await loadModelsController()
const ctx = makeCtx({ default: 'research-model', provider: 'deepseek' })
ctx.get = vi.fn((name: string) => name.toLowerCase() === 'x-hermes-profile' ? 'research' : '')
await setConfigModel(ctx)
expect(ctx.body).toEqual({ success: true })
const rootConfig = YAML.load(await readFile(join(hermesHome, 'config.yaml'), 'utf-8')) as any
const researchConfig = YAML.load(await readFile(join(researchDir, 'config.yaml'), 'utf-8')) as any
expect(rootConfig.model.default).toBe('root-model')
expect(researchConfig.model).toEqual({ default: 'research-model', provider: 'deepseek' })
})
it('skill toggle preserves unrelated config while adding and removing disabled skills', async () => {
await writeFile(join(hermesHome, 'config.yaml'), [
'model:',
@@ -185,6 +185,25 @@ describe('models controller — model visibility', () => {
]))
})
it('uses the requested profile for aggregate response defaults', async () => {
mockListProfileNamesFromDisk.mockReturnValue(['default', 'tester'])
mockReadConfigYamlForProfile.mockImplementation(async (profile: string) => ({
model: {
default: profile === 'tester' ? 'deepseek-reasoner' : 'deepseek-chat',
provider: 'deepseek',
},
}))
const ctx = makeCtx()
ctx.state = { user: { id: 1, username: 'admin', role: 'super_admin' } }
ctx.get = vi.fn((name: string) => name.toLowerCase() === 'x-hermes-profile' ? 'tester' : '')
await ctrl.getAvailable(ctx)
expect(ctx.body.default).toBe('deepseek-reasoner')
expect(ctx.body.default_provider).toBe('deepseek')
expect(ctx.body.profiles.map((profile: any) => profile.profile)).toEqual(['default', 'tester'])
})
it('uses explicit query profile for single-profile model fetches', async () => {
mockListProfileNamesFromDisk.mockReturnValue(['default', 'research'])
+55 -5
View File
@@ -168,6 +168,7 @@ describe('user auth tables and middleware', () => {
const user = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const token = auth.signUserJwt(user, 'test-secret')
const ctx = {
path: '/api/hermes/download',
headers: {},
query: { token },
state: {},
@@ -183,6 +184,46 @@ describe('user auth tables and middleware', () => {
expect(next).toHaveBeenCalledOnce()
})
it('lets SPA and static asset paths pass through without a JWT', async () => {
const { auth } = await initUsers()
const ctx = {
path: '/',
headers: {},
query: {},
state: {},
request: { body: {} },
status: 200,
body: null,
} as any
const next = vi.fn(async () => {})
await auth.requireUserJwt(ctx, next)
expect(next).toHaveBeenCalledOnce()
expect(ctx.status).toBe(200)
expect(ctx.body).toBeNull()
})
it('still requires a JWT for protected API paths', async () => {
const { auth } = await initUsers()
const ctx = {
path: '/api/hermes/sessions',
headers: {},
query: {},
state: {},
request: { body: {} },
status: 200,
body: null,
} as any
const next = vi.fn(async () => {})
await auth.requireUserJwt(ctx, next)
expect(next).not.toHaveBeenCalled()
expect(ctx.status).toBe(401)
expect(ctx.body).toEqual({ error: 'Unauthorized' })
})
it('bootstraps the default super admin through password login and returns a user JWT', async () => {
await initUsers()
const ctrl = await import('../../packages/server/src/controllers/auth')
@@ -200,7 +241,7 @@ describe('user auth tables and middleware', () => {
expect(ctx.body.token).toMatch(/^[^.]+\.[^.]+\.[^.]+$/)
})
it('marks the default account credentials as requiring a change', async () => {
it('marks only admin with password 123456 as requiring a credential change', async () => {
const { users } = await initUsers()
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
const ctrl = await import('../../packages/server/src/controllers/auth')
@@ -213,15 +254,24 @@ describe('user auth tables and middleware', () => {
await ctrl.currentUser(defaultCtx)
expect(defaultCtx.body.user.requiresCredentialChange).toBe(true)
users.updateUsername(admin.id, 'owner')
users.updateUserPassword(admin.id, 'stronger-password')
const changedCtx = {
const passwordChangedCtx = {
state: { user: { id: admin.id, username: 'admin', role: 'super_admin' } },
status: 200,
body: null,
} as any
await ctrl.currentUser(passwordChangedCtx)
expect(passwordChangedCtx.body.user.requiresCredentialChange).toBe(false)
users.updateUserPassword(admin.id, '123456')
users.updateUsername(admin.id, 'owner')
const usernameChangedCtx = {
state: { user: { id: admin.id, username: 'owner', role: 'super_admin' } },
status: 200,
body: null,
} as any
await ctrl.currentUser(changedCtx)
expect(changedCtx.body.user.requiresCredentialChange).toBe(false)
await ctrl.currentUser(usernameChangedCtx)
expect(usernameChangedCtx.body.user.requiresCredentialChange).toBe(false)
})
it('lets super admins create regular admins with profile bindings', async () => {