mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 13:30:14 +00:00
[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:
@@ -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
@@ -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 管理、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。
|
||||
|
||||
## 技术栈
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1119,6 +1119,14 @@ jobTriggered: 'Job declenche',
|
||||
new_0_5_35_6: 'Performance Monitor ne bloque plus sur les requêtes worker pendant l’initialisation 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 l’UI du chat avec les états démarrage, outil, progression et fin',
|
||||
new_0_6_0_5: 'L’arrêt ou l’abandon d’un 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
|
||||
|
||||
@@ -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 の単一ユーザー版へ戻してください',
|
||||
},
|
||||
|
||||
// ファイル
|
||||
|
||||
@@ -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 단일 사용자 버전으로 되돌리세요',
|
||||
},
|
||||
|
||||
// 파일
|
||||
|
||||
@@ -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 單使用者版本',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 单用户版本',
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '聊天流式通信',
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user