diff --git a/README.md b/README.md index 0275e5f1..3d45ff71 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_zh.md b/README_zh.md index 957a0abb..bad4e5cc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -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 管理、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。 ## 技术栈 diff --git a/bin/hermes-web-ui.mjs b/bin/hermes-web-ui.mjs index 654b45f3..0e11ba3f 100755 --- a/bin/hermes-web-ui.mjs +++ b/bin/hermes-web-ui.mjs @@ -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}`) } }) diff --git a/package.json b/package.json index b9abcb93..0e69a391 100644 --- a/package.json +++ b/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/client/src/data/changelog.ts b/packages/client/src/data/changelog.ts index 888b3670..9ce7785e 100644 --- a/packages/client/src/data/changelog.ts +++ b/packages/client/src/data/changelog.ts @@ -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', diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index bbe5bd6f..eff8c0df 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -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 diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 7263edbb..50e8ebcc 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -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', }, } diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index c4b34c71..09753b3e 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -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 diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index a5ba49cd..f820d804 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -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 diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 300092d6..582477e0 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -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 の単一ユーザー版へ戻してください', }, // ファイル diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index 7770291e..a6a6ab9f 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -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 단일 사용자 버전으로 되돌리세요', }, // 파일 diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index d33ae836..0a294380 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -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 diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts index 46ea1ef7..ddeb4f5d 100644 --- a/packages/client/src/i18n/locales/zh-TW.ts +++ b/packages/client/src/i18n/locales/zh-TW.ts @@ -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 單使用者版本', }, } diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 0cf0d527..b57f403f 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -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 单用户版本', }, } diff --git a/packages/client/src/stores/hermes/app.ts b/packages/client/src/stores/hermes/app.ts index 85ab2287..d55b1148 100644 --- a/packages/client/src/stores/hermes/app.ts +++ b/packages/client/src/stores/hermes/app.ts @@ -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 diff --git a/packages/server/src/controllers/auth.ts b/packages/server/src/controllers/auth.ts index 8f7cfde1..164625af 100644 --- a/packages/server/src/controllers/auth.ts +++ b/packages/server/src/controllers/auth.ts @@ -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), }, } } diff --git a/packages/server/src/controllers/hermes/models.ts b/packages/server/src/controllers/hermes/models.ts index 062e85ab..0fd12869 100644 --- a/packages/server/src/controllers/hermes/models.ts +++ b/packages/server/src/controllers/hermes/models.ts @@ -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) diff --git a/packages/server/src/middleware/user-auth.ts b/packages/server/src/middleware/user-auth.ts index 515f4e95..58617dcc 100644 --- a/packages/server/src/middleware/user-auth.ts +++ b/packages/server/src/middleware/user-auth.ts @@ -83,6 +83,13 @@ async function allowServerTokenForMedia(ctx: Context, token: string): Promise, secret: string, now = Date.now()): string { const iat = Math.floor(now / 1000) const payload: JwtPayload = { @@ -154,6 +161,11 @@ export async function isAuthEnabled(): Promise { } export async function requireUserJwt(ctx: Context, next: Next): Promise { + if (!isProtectedHttpPath(ctx.path)) { + await next() + return + } + const secret = await getJwtSecret() if (!secret) { await next() diff --git a/packages/website/src/i18n/en.ts b/packages/website/src/i18n/en.ts index 4619e103..39c46891 100644 --- a/packages/website/src/i18n/en.ts +++ b/packages/website/src/i18n/en.ts @@ -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', diff --git a/packages/website/src/i18n/zh.ts b/packages/website/src/i18n/zh.ts index b0f18e92..b5777759 100644 --- a/packages/website/src/i18n/zh.ts +++ b/packages/website/src/i18n/zh.ts @@ -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: '聊天流式通信', diff --git a/tests/client/app-store.test.ts b/tests/client/app-store.test.ts index 321ec2f7..e5ec11bc 100644 --- a/tests/client/app-store.test.ts +++ b/tests/client/app-store.test.ts @@ -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: '', diff --git a/tests/server/config-mutating-controllers.test.ts b/tests/server/config-mutating-controllers.test.ts index ff0a4c64..1fe53f8e 100644 --- a/tests/server/config-mutating-controllers.test.ts +++ b/tests/server/config-mutating-controllers.test.ts @@ -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:', diff --git a/tests/server/model-visibility-controller.test.ts b/tests/server/model-visibility-controller.test.ts index 86b32003..8f27a7f1 100644 --- a/tests/server/model-visibility-controller.test.ts +++ b/tests/server/model-visibility-controller.test.ts @@ -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']) diff --git a/tests/server/user-auth.test.ts b/tests/server/user-auth.test.ts index 2e2a54f5..0f45fa4b 100644 --- a/tests/server/user-auth.test.ts +++ b/tests/server/user-auth.test.ts @@ -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 () => {