From 634a622934ded6aeaceadf987b676c30ae93cad9 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Sun, 24 May 2026 12:52:14 +0800 Subject: [PATCH] [codex] fix media skill profile auth and run events (#965) * fix media skill profile auth and run events * test bridge run profile context --- README.md | 24 +++--- README_zh.md | 24 +++--- docs/cli-chat-sessions.md | 8 +- docs/docker.md | 2 +- docs/openapi.json | 52 +++++------ packages/client/src/api/hermes/chat.ts | 20 +++++ packages/client/src/stores/hermes/chat.ts | 86 +++++++++++++++++++ .../server/src/controllers/hermes/media.ts | 76 +++++++++++++--- packages/server/src/middleware/user-auth.ts | 18 ++++ .../hermes/agent-bridge/hermes_bridge.py | 14 ++- .../hermes/group-chat/agent-clients.ts | 5 ++ .../src/services/hermes/run-chat/abort.ts | 5 -- .../hermes/run-chat/handle-api-run.ts | 1 + .../hermes/run-chat/handle-bridge-run.ts | 43 +++++++++- .../src/services/hermes/run-chat/index.ts | 1 + packages/skills/apikey-image-gen/SKILL.md | 24 +++++- packages/skills/grok-image-to-video/SKILL.md | 18 +++- packages/website/src/i18n/en.ts | 20 ++--- packages/website/src/i18n/zh.ts | 20 ++--- .../run-chat-bridge-final-context.test.ts | 4 +- 20 files changed, 368 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index ec528b3..0275e5f 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ - Sessions sorted by latest message time - Markdown rendering with syntax highlighting and code copy - Tool call detail expansion (arguments / result) -- File upload support -- File download support — download user-uploaded files and agent-generated files across local, Docker, SSH, and Singularity backends +- Profile-scoped file uploads +- File download support — download uploaded files and agent-generated files by resolved path across local, Docker, SSH, and Singularity backends - Session search — Ctrl+K search across the Web UI local session database; read-only Hermes history sessions are not included -- Global model selector — discovers models from `~/.hermes/auth.json` credential pool +- Profile-aware model selector — discovers models available to the signed-in account through authorized Hermes profiles - Per-session model display badge and context token usage ### Platform Channels @@ -94,12 +94,14 @@ Unified configuration for **8 platforms** in one page: - Create, rename, delete, and switch between Hermes profiles - Clone existing profile or import from archive (`.tar.gz`) - Export profile for backup or sharing -- Profile-scoped configuration and cache isolation +- Profile-scoped configuration, cache, uploads, sessions, jobs, usage, memory, skills, plugins, providers, and model visibility +- Account-bound profile access: super administrators can manage every profile; regular administrators only see and use profiles assigned to their account ### File Browser - Browse files on remote backends (local, Docker, SSH, Singularity) - Upload, download, rename, copy, move, and delete files +- Store uploaded files under the selected/requested Hermes profile while keeping downloads path-based for agent-generated artifacts outside the upload directory - Create directories - View file content with syntax highlighting @@ -129,7 +131,9 @@ Unified configuration for **8 platforms** in one page: ### Authentication - Token-based auth (auto-generated on first run or set via `AUTH_TOKEN` env var) -- Optional username/password login — set via settings page after initial token auth +- Username/password login with account management in Settings +- Default bootstrap credentials are `admin` / `123456`; users are prompted after login to change the default username and password +- Super administrators can manage users and profile bindings; regular administrators can manage their own account details - Auth can be disabled with `AUTH_DISABLED=1` ### Settings @@ -219,11 +223,11 @@ These variables configure Hermes Web UI itself. Provider API keys and Hermes Age | `PORT` | `8648` | Web UI listen port. | | `BIND_HOST` | `0.0.0.0` | Web UI bind host. Set `::` explicitly for IPv6. | | `HERMES_WEB_UI_HOME` | `~/.hermes-web-ui` | Web UI data home for auth token, credentials, logs, DB, and default uploads. `HERMES_WEBUI_STATE_DIR` is also supported as a compatibility alias. | -| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | Upload directory override. | +| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | Upload root override. Files are stored below profile-scoped subdirectories. | | `CORS_ORIGINS` | `*` | Koa CORS origin setting. | | `AUTH_DISABLED` | unset | Set to `1` or `true` to disable Web UI auth. | | `AUTH_TOKEN` | auto-generated | Explicit bearer token. If unset, Web UI creates one under `HERMES_WEB_UI_HOME`. | -| `PROFILE` | `default` | Initial Hermes profile name. | +| `PROFILE` | `default` | Startup/default Hermes profile. Runtime requests use the profile selected by the frontend and authorized for the current account. | | `LOG_LEVEL` | `info` | Server log level. | | `BRIDGE_LOG_LEVEL` | `$LOG_LEVEL` or `info` | Bridge log level. | | `MAX_DOWNLOAD_SIZE` | `200MB` | Maximum file download size. | @@ -282,14 +286,14 @@ Browser → BFF (Koa, :8648) → Socket.IO /chat-run Hermes agent bridge → Hermes Agent runtime ↓ Hermes CLI / profiles - ~/.hermes/config.yaml (channel behavior) - ~/.hermes/auth.json (credential pool) + profile config.yaml (channel/provider behavior) + profile auth.json (credential pool) Tencent iLink API (WeChat QR login) ``` 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, file upload and download (multi-backend: local/Docker/SSH/Singularity), session CRUD, 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/profile authorization, 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 692926c..957a0ab 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,10 +49,10 @@ - 按最新消息时间排序会话列表 - Markdown 渲染,支持语法高亮和代码复制 - 工具调用详情展开(参数 / 结果) -- 文件上传支持 -- 文件下载支持 — 支持下载用户上传的文件和 Agent 生成的文件,兼容 local、Docker、SSH、Singularity 等多种 terminal backend +- 按 Profile 隔离的文件上传 +- 文件下载支持 — 按解析后的路径下载用户上传文件和 Agent 生成文件,兼容 local、Docker、SSH、Singularity 等多种 terminal backend - 会话搜索 — Ctrl+K 搜索 Web UI 本地会话库;不包含只读 Hermes 历史会话 -- 全局模型选择器 — 自动从 `~/.hermes/auth.json` 凭证池发现可用模型 +- 按账号授权 Profile 汇总模型选择器 — 只展示当前账号可访问的 Hermes Profile 中可用的模型 - 每个会话显示模型标签和上下文 Token 用量 ### 平台渠道 @@ -102,12 +102,14 @@ - 创建、重命名、删除、切换 Hermes 配置文件(Profile) - 克隆现有配置文件或从归档导入(`.tar.gz`) - 导出配置文件用于备份或分享 -- 配置文件级别的配置和缓存隔离 +- 按 Profile 隔离配置、缓存、上传、会话、任务、用量、记忆、技能、插件、Provider 和模型可见性 +- 账号绑定 Profile 权限:超级管理员可以管理全部 Profile;普通管理员只能查看和使用分配给自己的 Profile ### 文件浏览器 - 浏览远程后端文件(local、Docker、SSH、Singularity) - 上传、下载、重命名、复制、移动和删除文件 +- 上传文件保存到当前选择/请求的 Hermes Profile 目录下;下载按真实路径解析,支持下载上传目录外的 Agent 产物 - 创建目录 - 查看文件内容,支持语法高亮 @@ -137,7 +139,9 @@ ### 认证 - 基于 Token 的认证(首次运行自动生成或通过 `AUTH_TOKEN` 环境变量设置) -- 可选的用户名/密码登录 — 通过初始 Token 认证后在设置页面设置 +- 用户名/密码登录,并在设置页提供账户管理 +- 默认登录名/密码为 `admin` / `123456`;登录后会提示尽快修改默认账户和密码 +- 超级管理员可以管理用户和 Profile 绑定;普通管理员只能管理自己的账户信息 - 可通过 `AUTH_DISABLED=1` 禁用认证 ### 设置 @@ -226,11 +230,11 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源 | `PORT` | `8648` | Web UI 监听端口。 | | `BIND_HOST` | `0.0.0.0` | Web UI 绑定地址。如需 IPv6,可显式设置为 `::`。 | | `HERMES_WEB_UI_HOME` | `~/.hermes-web-ui` | Web UI 数据目录,用于认证 token、登录凭据、日志、数据库和默认上传目录。兼容支持 `HERMES_WEBUI_STATE_DIR` 作为别名。 | -| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | 覆盖上传目录。 | +| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | 覆盖上传根目录。文件会保存在按 Profile 隔离的子目录下。 | | `CORS_ORIGINS` | `*` | Koa CORS origin 配置。 | | `AUTH_DISABLED` | 未设置 | 设置为 `1` 或 `true` 可关闭 Web UI 认证。 | | `AUTH_TOKEN` | 自动生成 | 显式指定 bearer token。未设置时,Web UI 会在 `HERMES_WEB_UI_HOME` 下自动生成。 | -| `PROFILE` | `default` | 初始 Hermes profile 名称。 | +| `PROFILE` | `default` | 启动/默认 Hermes profile。运行时请求使用前端当前选择且当前账号有权限访问的 Profile。 | | `LOG_LEVEL` | `info` | Server 日志级别。 | | `BRIDGE_LOG_LEVEL` | `$LOG_LEVEL` 或 `info` | Bridge 日志级别。 | | `MAX_DOWNLOAD_SIZE` | `200MB` | 最大文件下载大小。 | @@ -289,14 +293,14 @@ npm run build # 构建输出到 dist/ Hermes agent bridge → Hermes Agent runtime ↓ Hermes CLI / profiles - ~/.hermes/config.yaml (渠道行为配置) - ~/.hermes/auth.json (凭证池) + profile config.yaml (渠道/Provider 配置) + profile auth.json (凭证池) 腾讯 iLink API (微信扫码登录) ``` 前端采用 **多 Agent 可扩展架构** — 所有 Hermes 相关代码都按命名空间组织在 `hermes/` 目录下(API、组件、视图、Store),可以方便地并行接入新的 Agent。 -BFF 层负责:Socket.IO 聊天流式推送、Hermes agent bridge、文件上传与下载(多 Backend 支持:local/Docker/SSH/Singularity)、会话 CRUD、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。 +BFF 层负责:Socket.IO 聊天流式推送、Hermes agent bridge、按 Profile 隔离的上传和按路径解析的下载(多 Backend 支持:local/Docker/SSH/Singularity)、会话 CRUD、账号/Profile 鉴权、配置/凭证管理、微信扫码登录、模型发现、技能/记忆管理、日志读取和静态文件服务。 ## 技术栈 diff --git a/docs/cli-chat-sessions.md b/docs/cli-chat-sessions.md index 1adbcd4..e5c479e 100644 --- a/docs/cli-chat-sessions.md +++ b/docs/cli-chat-sessions.md @@ -87,7 +87,7 @@ ChatRunSocket (Node.js) | `packages/server/src/index.ts` | 启动 `AgentBridgeManager` 和 `ChatRunSocket` | | `packages/server/src/services/shutdown.ts` | 关闭时停止 chat socket 和 bridge 子进程 | | `packages/server/src/controllers/hermes/sessions.ts` | 会话列表和详情读取,包含 `source` 信息 | -| `packages/server/src/controllers/hermes/profiles.ts` | profile 切换/管理时清理 bridge 内存会话 | +| `packages/server/src/controllers/hermes/profiles.ts` | profile 管理接口;按 URL/body 中的 profile 做权限校验 | ### 已移除的旧文件 @@ -302,7 +302,7 @@ Windows 使用 TCP 是因为部分 Python/Windows 环境没有 Unix domain socke | `get_output` | 通过 `cursor` 和 `event_cursor` 获取增量文本与事件 | | `interrupt` | 调用 agent 中断当前运行 | | `approval_respond` | 响应工具审批 | -| `destroy_all` | profile 切换/管理时销毁全部 bridge 内存 session | +| `destroy_all` | 维护动作;仅用于明确的全量清理/进程关闭场景,普通 profile 切换不会调用 | bridge 代码里还保留了一些调试/维护 action,例如 `ping`、`get_result`、`get_history`、`destroy`、`list`、`shutdown`、`steer`。当前 `/chat-run` 前端路径不会直接暴露这些 action;需要的能力由 Node `/chat-run` 层封装,例如 `/steer` slash command 会调用 `steer` action。 @@ -313,7 +313,7 @@ bridge 代码里还保留了一些调试/维护 action,例如 `ping`、`get_re `AgentPool` 维护 `session_id -> AgentSession`: - 每个 session 持有独立 `AIAgent` 实例。 -- session 按 profile 创建,profile 改变时会重建对应 agent。 +- session 按请求中的 profile 创建和复用;前端切换 Hermes Profile 只改变后续请求使用的 profile,不会影响其他 bridge 内存 session。 - `HERMES_HOME` 会在创建 agent 时临时切到 profile home。 - `SessionDB` 按 profile 的 `state.db` 路径缓存。 - 空闲 session 会被 bridge GC,默认 30 分钟无运行后销毁内存态。 @@ -449,7 +449,7 @@ chatRunServer.init() | `HERMES_BRIDGE_MAX_TURNS` | 覆盖 bridge 最大轮数 | | `UV` | uv 可执行文件路径 fallback | -正常使用不需要配置这些变量。Windows 下如果默认 TCP 端口被旧 bridge/broker/worker 占用,新 bridge 会先按端口杀掉旧进程树,再用同一个 endpoint 重建。 +正常使用不需要配置这些变量。Bridge 支持多个用户/多个 profile 的运行并存;Web UI 的 Hermes Profile 切换不会重启 bridge 或销毁其他正在运行的任务。Windows 下如果默认 TCP 端口被旧 bridge/broker/worker 占用,新 bridge 会先按端口杀掉旧进程树,再用同一个 endpoint 重建。 Windows 首次启动慢时可以临时放大: diff --git a/docs/docker.md b/docs/docker.md index 53710c8..427a563 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -82,7 +82,7 @@ No Hermes gateway ports are exposed by this compose setup. - Hermes CLI binary comes from `HERMES_BIN` env (`packages/server/src/services/hermes-cli.ts`). - If `HERMES_BIN` is not provided, code falls back to `hermes` in `PATH`. -- Profile-specific chat runs are handled through the Hermes agent bridge. +- Profile-specific chat runs are handled through the Hermes agent bridge. The selected/requested profile is authorized per account and passed with runtime requests; switching the frontend Hermes Profile does not restart the bridge or clear other running tasks. - The Web UI does not automatically start or manage a Hermes Agent gateway process on startup. ## Common Operations diff --git a/docs/openapi.json b/docs/openapi.json index e692e24..fce809f 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -231,6 +231,32 @@ } } }, + "/api/auth/me": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Get me", + "description": "GET /api/auth/me", + "operationId": "currentUser", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "description": "Not found" + } + } + } + }, "/api/auth/password": { "delete": { "tags": [ @@ -2004,32 +2030,6 @@ } } }, - "/api/hermes/profiles/active": { - "put": { - "tags": [ - "Profiles" - ], - "summary": "Update active", - "description": "PUT /api/hermes/profiles/active", - "operationId": "switchProfile", - "security": [ - { - "BearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, "/api/hermes/profiles/import": { "post": { "tags": [ diff --git a/packages/client/src/api/hermes/chat.ts b/packages/client/src/api/hermes/chat.ts index 26fbcb7..2e9c6c8 100644 --- a/packages/client/src/api/hermes/chat.ts +++ b/packages/client/src/api/hermes/chat.ts @@ -91,6 +91,7 @@ const sessionEventHandlers = new Map void onToolStarted: (event: RunEvent) => void onToolCompleted: (event: RunEvent) => void + onSubagentEvent?: (event: RunEvent) => void onRunStarted: (event: RunEvent) => void onRunCompleted: (event: RunEvent) => void onRunFailed: (event: RunEvent) => void @@ -187,6 +188,16 @@ function globalToolCompletedHandler(event: RunEvent): void { } } +function globalSubagentEventHandler(event: RunEvent): void { + const sid = event.session_id + if (!sid) return + + const handlers = sessionEventHandlers.get(sid) + if (handlers?.onSubagentEvent) { + handlers.onSubagentEvent(event) + } +} + /** * Global run.started event handler */ @@ -376,6 +387,7 @@ export function registerSessionHandlers( onReasoningAvailable: (event: RunEvent) => void onToolStarted: (event: RunEvent) => void onToolCompleted: (event: RunEvent) => void + onSubagentEvent?: (event: RunEvent) => void onRunStarted: (event: RunEvent) => void onRunCompleted: (event: RunEvent) => void onRunFailed: (event: RunEvent) => void @@ -485,6 +497,10 @@ export function connectChatRun(requestedProfile?: string | null): Socket { // Tool events chatRunSocket.on('tool.started', globalToolStartedHandler) chatRunSocket.on('tool.completed', globalToolCompletedHandler) + chatRunSocket.on('subagent.start', globalSubagentEventHandler) + chatRunSocket.on('subagent.tool', globalSubagentEventHandler) + chatRunSocket.on('subagent.progress', globalSubagentEventHandler) + chatRunSocket.on('subagent.complete', globalSubagentEventHandler) // Run lifecycle events chatRunSocket.on('run.started', globalRunStartedHandler) @@ -622,6 +638,10 @@ export function startRunViaSocket( if (closed) return onEvent(evt) }, + onSubagentEvent: (evt: RunEvent) => { + if (closed) return + onEvent(evt) + }, onRunStarted: (evt: RunEvent) => { if (closed) return onEvent(evt) diff --git a/packages/client/src/stores/hermes/chat.ts b/packages/client/src/stores/hermes/chat.ts index 27ed79a..6f2727b 100644 --- a/packages/client/src/stores/hermes/chat.ts +++ b/packages/client/src/stores/hermes/chat.ts @@ -667,6 +667,8 @@ export const useChatStore = defineStore('chat', () => { toolResult: output, }) } + } else if (String(e.event || '').startsWith('subagent.')) { + handleSubagentEvent(sessionId, e as RunEvent) } } } @@ -757,6 +759,71 @@ export const useChatStore = defineStore('chat', () => { } } + function handleSubagentEvent(sessionId: string, evt: RunEvent) { + const eventName = String(evt.event || '') + if (!eventName.startsWith('subagent.')) return + + const subagentId = String((evt as any).subagent_id || `${(evt as any).task_index ?? 0}`) + const toolCallId = `subagent:${evt.run_id || 'run'}:${subagentId}` + const taskIndex = Number((evt as any).task_index ?? 0) + const taskCount = Number((evt as any).task_count ?? 1) + const label = `${taskIndex + 1}/${Math.max(1, taskCount || 1)}` + const toolName = String((evt as any).tool || (evt as any).name || '') + const toolCount = Number((evt as any).tool_count || 0) + const goal = String((evt as any).goal || '').trim() + const text = String(evt.text || evt.preview || '').trim() + const summary = String((evt as any).summary || '').trim() + const duration = Number((evt as any).duration_seconds ?? (evt as any).duration) + + let preview = text || summary || goal + if (eventName === 'subagent.start') { + preview = `subagent ${label} started${goal ? `: ${goal}` : ''}` + } else if (eventName === 'subagent.tool') { + const prefix = `subagent ${label}${toolCount ? ` turn ${toolCount}` : ''}` + preview = `${prefix}${toolName ? `: ${toolName}` : ''}${text ? ` - ${text}` : ''}` + } else if (eventName === 'subagent.progress') { + preview = `subagent ${label}: ${text || 'working'}` + } else if (eventName === 'subagent.complete') { + const status = String((evt as any).status || 'completed') + preview = `subagent ${label} ${status}${summary ? `: ${summary}` : ''}` + } + + const msgs = getSessionMsgs(sessionId) + const existing = msgs.find(m => m.role === 'tool' && m.toolCallId === toolCallId) + const toolStatus = eventName === 'subagent.complete' + ? ((evt as any).status && String((evt as any).status) !== 'completed' ? 'error' : 'done') + : 'running' + const update: Partial = { + toolName: 'delegate_task', + toolCallId, + toolPreview: preview.slice(0, 220), + toolStatus, + toolDuration: Number.isFinite(duration) ? duration : undefined, + toolResult: eventName === 'subagent.complete' + ? JSON.stringify({ + status: (evt as any).status || 'completed', + summary: summary || text, + api_calls: (evt as any).api_calls, + input_tokens: (evt as any).input_tokens, + output_tokens: (evt as any).output_tokens, + }, null, 2) + : undefined, + } + + if (existing) { + updateMessage(sessionId, existing.id, update) + return + } + + addMessage(sessionId, { + id: uid(), + role: 'tool', + content: '', + timestamp: Date.now(), + ...update, + }) + } + function addAgentErrorMessage(sessionId: string, error?: string | null) { const content = error ? `Error: ${error}` : 'Run failed' const msgs = getSessionMsgs(sessionId) @@ -1383,6 +1450,15 @@ export const useChatStore = defineStore('chat', () => { break } + case 'subagent.start': + case 'subagent.tool': + case 'subagent.progress': + case 'subagent.complete': { + runHadToolActivity = true + handleSubagentEvent(sid, evt) + break + } + case 'approval.requested': { setPendingApproval(evt) break @@ -1824,6 +1900,15 @@ export const useChatStore = defineStore('chat', () => { break } + case 'subagent.start': + case 'subagent.tool': + case 'subagent.progress': + case 'subagent.complete': { + runHadToolActivity = true + handleSubagentEvent(sid, evt) + break + } + case 'approval.requested': { setPendingApproval(evt) break @@ -1971,6 +2056,7 @@ export const useChatStore = defineStore('chat', () => { onReasoningAvailable: (evt) => handleEvent(evt), onToolStarted: (evt) => handleEvent(evt), onToolCompleted: (evt) => handleEvent(evt), + onSubagentEvent: (evt) => handleEvent(evt), onRunStarted: (evt) => handleEvent(evt), onRunCompleted: (evt) => handleEvent(evt), onRunFailed: (evt) => handleEvent(evt), diff --git a/packages/server/src/controllers/hermes/media.ts b/packages/server/src/controllers/hermes/media.ts index bf31c6e..6070583 100644 --- a/packages/server/src/controllers/hermes/media.ts +++ b/packages/server/src/controllers/hermes/media.ts @@ -1,9 +1,9 @@ import type { Context } from 'koa' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { dirname, extname, isAbsolute, join, resolve } from 'path' -import { getActiveAuthPath } from '../../services/hermes/hermes-profile' +import { getActiveProfileName, getProfileDir, listProfileNamesFromDisk } from '../../services/hermes/hermes-profile' import { config } from '../../config' -import { readConfigYaml } from '../../services/config-helpers' +import { readConfigYamlForProfile } from '../../services/config-helpers' const XAI_VIDEO_GENERATIONS_URL = 'https://api.x.ai/v1/videos/generations' const XAI_VIDEO_STATUS_URL = 'https://api.x.ai/v1/videos' @@ -28,6 +28,42 @@ type FunCodexProvider = { model: string } +function requestedProfileName(ctx: Context): string { + const headerProfile = ctx.get('x-hermes-profile') + const queryProfile = typeof ctx.query.profile === 'string' ? ctx.query.profile : '' + const body = ctx.request.body as { profile?: unknown } | undefined + const bodyProfile = typeof body?.profile === 'string' ? body.profile : '' + return (ctx.state.profile?.name || headerProfile || queryProfile || bodyProfile || '').trim() +} + +function resolveMediaProfile(ctx: Context): string { + let requested = requestedProfileName(ctx) + if (!requested && ctx.state.user?.role !== 'super_admin' && !ctx.state.serverTokenAuth) { + const profiles = ctx.state.user?.profiles || [] + if (profiles.length === 1) { + requested = profiles[0] + } else { + const err: any = new Error('Profile is required') + err.status = 400 + err.code = 'profile_required' + throw err + } + } + + const profile = requested || getActiveProfileName() || 'default' + if (!listProfileNamesFromDisk().includes(profile)) { + const err: any = new Error(`Profile "${profile}" does not exist`) + err.status = 404 + err.code = 'profile_not_found' + throw err + } + return profile +} + +function authPathForProfile(profile: string): string { + return join(getProfileDir(profile), 'auth.json') +} + function readJsonFile(path: string): any { try { return JSON.parse(readFileSync(path, 'utf-8')) @@ -43,8 +79,8 @@ function buildApiUrl(baseUrl: string, pathWithV1: string): string { return `${base}${apiPath}` } -async function resolveFunCodexProvider(): Promise { - const hermesConfig = await readConfigYaml() +async function resolveFunCodexProvider(profile: string): Promise { + const hermesConfig = await readConfigYamlForProfile(profile) const customProviders = Array.isArray(hermesConfig.custom_providers) ? hermesConfig.custom_providers as any[] : [] @@ -59,11 +95,11 @@ async function resolveFunCodexProvider(): Promise { } } -function resolveXaiToken(): { token: string; source: string } | null { +function resolveXaiToken(profile: string): { token: string; source: string } | null { const envToken = String(process.env.XAI_API_KEY || '').trim() if (envToken) return { token: envToken, source: 'XAI_API_KEY' } - const auth = readJsonFile(getActiveAuthPath()) as AuthJson | null + const auth = readJsonFile(authPathForProfile(profile)) as AuthJson | null const providerToken = String(auth?.providers?.['xai-oauth']?.tokens?.access_token || auth?.providers?.['xai-oauth']?.access_token || '').trim() if (providerToken) return { token: providerToken, source: 'xai-oauth' } @@ -421,11 +457,20 @@ function saveGeneratedImages(images: string[], requestedOutputPath?: string): st } export async function apiKeyImageGenerate(ctx: Context) { - const provider = await resolveFunCodexProvider() + let profile: string + try { + profile = resolveMediaProfile(ctx) + } catch (err: any) { + ctx.status = err.status || 400 + ctx.body = { error: err.message || String(err), code: err.code || 'invalid_profile' } + return + } + + const provider = await resolveFunCodexProvider(profile) if (!provider) { ctx.status = 401 ctx.body = { - error: 'Missing fun-codex provider in active profile config.yaml.', + error: `Missing fun-codex provider in profile "${profile}" config.yaml.`, code: 'missing_fun_codex_provider', } return @@ -443,6 +488,7 @@ export async function apiKeyImageGenerate(ctx: Context) { output_paths: outputPaths, provider: APIKEY_IMAGE_PROVIDER, base_url: provider.baseUrl, + profile, } } catch (err: any) { ctx.status = err.status || 500 @@ -484,11 +530,20 @@ async function downloadVideo(url: string, outputPath: string): Promise { } export async function grokImageToVideo(ctx: Context) { - const tokenInfo = resolveXaiToken() + let profile: string + try { + profile = resolveMediaProfile(ctx) + } catch (err: any) { + ctx.status = err.status || 400 + ctx.body = { error: err.message || String(err), code: err.code || 'invalid_profile' } + return + } + + const tokenInfo = resolveXaiToken(profile) if (!tokenInfo) { ctx.status = 401 ctx.body = { - error: 'Missing xAI token. Set XAI_API_KEY or complete xAI OAuth login first.', + error: `Missing xAI token for profile "${profile}". Set XAI_API_KEY or complete xAI OAuth login first.`, code: 'missing_xai_token', } return @@ -539,6 +594,7 @@ export async function grokImageToVideo(ctx: Context) { video_url: videoUrl, output_path: outputPath, token_source: tokenInfo.source, + profile, } return } diff --git a/packages/server/src/middleware/user-auth.ts b/packages/server/src/middleware/user-auth.ts index a3644c2..515f4e9 100644 --- a/packages/server/src/middleware/user-auth.ts +++ b/packages/server/src/middleware/user-auth.ts @@ -35,6 +35,7 @@ declare module 'koa' { interface DefaultState { user?: AuthenticatedUser profile?: RequestProfile + serverTokenAuth?: boolean } } @@ -69,6 +70,19 @@ function requestToken(ctx: Context): string { return typeof ctx.query.token === 'string' ? ctx.query.token.trim() : '' } +const SERVER_TOKEN_MEDIA_PATHS = new Set([ + '/api/hermes/media/apikey-image-generate', + '/api/hermes/media/grok-image-to-video', +]) + +async function allowServerTokenForMedia(ctx: Context, token: string): Promise { + if (!token || !SERVER_TOKEN_MEDIA_PATHS.has(ctx.path)) return false + const serverToken = await getToken() + if (!serverToken || token !== serverToken) return false + ctx.state.serverTokenAuth = true + return true +} + export function signUserJwt(user: Pick, secret: string, now = Date.now()): string { const iat = Math.floor(now / 1000) const payload: JwtPayload = { @@ -149,6 +163,10 @@ export async function requireUserJwt(ctx: Context, next: Next): Promise { const token = requestToken(ctx) const payload = token ? verifyUserJwt(token, secret) : null if (!payload) { + if (await allowServerTokenForMedia(ctx, token)) { + await next() + return + } ctx.status = 401 ctx.body = { error: 'Unauthorized' } return diff --git a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py index 594d106..9964f71 100755 --- a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py +++ b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py @@ -948,7 +948,7 @@ class AgentPool: def _tool_progress_callback(self, session_id: str): def callback(event_type, function_name=None, preview=None, function_args=None, **kwargs): - if event_type in (None, "tool.started", "tool.completed"): + if event_type in (None, "tool.started", "tool.completed") or str(event_type or "").startswith("subagent."): print( "[hermes_bridge] tool_progress_callback " f"session={session_id} event={event_type} tool={function_name} " @@ -964,6 +964,18 @@ class AgentPool: }) return + if str(event_type or "").startswith("subagent."): + payload = { + "event": str(event_type), + "tool_name": str(function_name) if function_name else "", + "text": str(preview) if preview is not None else "", + "args": _jsonable(function_args) if function_args else {}, + } + for key, value in kwargs.items(): + payload[str(key)] = _jsonable(value) + self._append_event(session_id, payload) + return + if event_type == "_thinking": text = function_name if text: diff --git a/packages/server/src/services/hermes/group-chat/agent-clients.ts b/packages/server/src/services/hermes/group-chat/agent-clients.ts index 66fc698..a80bdb1 100644 --- a/packages/server/src/services/hermes/group-chat/agent-clients.ts +++ b/packages/server/src/services/hermes/group-chat/agent-clients.ts @@ -473,6 +473,11 @@ class AgentClient { return { ...block, text: `${routedPrefix}\n\n原始消息:${text || msg.content}` } }) : `${routedPrefix}\n\n原始消息:${stripMentionRoutingTokens(msg.content, this.name) || msg.content}` + const runContext = [ + `[Current Hermes profile: ${this.profile}]`, + 'When calling Hermes Web UI endpoints from tools or skills, include the current Hermes profile as the X-Hermes-Profile header if the endpoint supports profile-scoped behavior.', + ].join('\n') + instructions = instructions ? `${runContext}\n${instructions}` : runContext const bridgeInput: AgentBridgeMessage = isContentBlockArray(input) ? await convertContentBlocksForAgent(input) : input diff --git a/packages/server/src/services/hermes/run-chat/abort.ts b/packages/server/src/services/hermes/run-chat/abort.ts index 60507d7..53e6a14 100644 --- a/packages/server/src/services/hermes/run-chat/abort.ts +++ b/packages/server/src/services/hermes/run-chat/abort.ts @@ -127,11 +127,6 @@ export async function markAbortCompleted( } state.events = [] - replaceState(sessionMap, sessionId, 'abort.completed', { - event: 'abort.completed', - run_id: runId, - synced: true, - }) emitToSession(nsp, socket, sessionId, 'abort.completed', { event: 'abort.completed', run_id: runId, diff --git a/packages/server/src/services/hermes/run-chat/handle-api-run.ts b/packages/server/src/services/hermes/run-chat/handle-api-run.ts index 650eb3f..3334400 100644 --- a/packages/server/src/services/hermes/run-chat/handle-api-run.ts +++ b/packages/server/src/services/hermes/run-chat/handle-api-run.ts @@ -105,6 +105,7 @@ export async function handleApiRun( sessionMap.set(session_id, state) } state.isWorking = true + state.events = [] state.profile = profile state.source = 'api_server' state.activeRunMarker = runMarker diff --git a/packages/server/src/services/hermes/run-chat/handle-bridge-run.ts b/packages/server/src/services/hermes/run-chat/handle-bridge-run.ts index 2025340..66a9958 100644 --- a/packages/server/src/services/hermes/run-chat/handle-bridge-run.ts +++ b/packages/server/src/services/hermes/run-chat/handle-bridge-run.ts @@ -128,10 +128,12 @@ export async function handleBridgeRun( if (resolvedProvider && sessionRow.provider !== resolvedProvider) updates.provider = resolvedProvider if (Object.keys(updates).length > 0) updateSession(session_id, updates) } - if (sessionRow?.workspace) { - const workspaceCtx = `[Current working directory: ${sessionRow.workspace}]` - fullInstructions = `\n${workspaceCtx}\n${fullInstructions}` - } + const runContext = [ + `[Current Hermes profile: ${profile}]`, + sessionRow?.workspace ? `[Current working directory: ${sessionRow.workspace}]` : '', + 'When calling Hermes Web UI endpoints from tools or skills, include the current Hermes profile as the X-Hermes-Profile header if the endpoint supports profile-scoped behavior.', + ].filter(Boolean).join('\n') + fullInstructions = `\n${runContext}\n${fullInstructions}` const runMarker = `cli_run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}` const now = Math.floor(Date.now() / 1000) @@ -145,6 +147,7 @@ export async function handleBridgeRun( state.isWorking = true state.isAborting = false + state.events = [] state.profile = profile state.source = 'cli' state.activeRunMarker = runMarker @@ -491,6 +494,38 @@ async function applyBridgeChunkAsync( } pushState(sessionMap, sessionId, 'tool.completed', payload) emit('tool.completed', payload) + } else if (evType?.startsWith('subagent.')) { + const payload = { + event: evType, + run_id: chunk.run_id, + subagent_id: ev.subagent_id, + parent_id: ev.parent_id, + depth: ev.depth, + task_index: ev.task_index, + task_count: ev.task_count, + goal: ev.goal, + model: ev.model, + toolsets: ev.toolsets, + tool_count: ev.tool_count, + tool: ev.tool_name, + name: ev.tool_name, + preview: ev.text || ev.summary || ev.tool_preview || '', + text: ev.text || '', + status: ev.status, + summary: ev.summary, + duration: ev.duration_seconds, + duration_seconds: ev.duration_seconds, + input_tokens: ev.input_tokens, + output_tokens: ev.output_tokens, + reasoning_tokens: ev.reasoning_tokens, + api_calls: ev.api_calls, + cost_usd: ev.cost_usd, + files_read: ev.files_read, + files_written: ev.files_written, + output_tail: ev.output_tail, + } + pushState(sessionMap, sessionId, evType, payload) + emit(evType, payload) } else if (evType === 'turn.boundary') { flushBridgePendingToDb(state, sessionId, runMarker) } else if (evType === 'reasoning.delta' || evType === 'thinking.delta') { diff --git a/packages/server/src/services/hermes/run-chat/index.ts b/packages/server/src/services/hermes/run-chat/index.ts index 24b954d..4af5e76 100644 --- a/packages/server/src/services/hermes/run-chat/index.ts +++ b/packages/server/src/services/hermes/run-chat/index.ts @@ -168,6 +168,7 @@ export class ChatRunSocket { logger.info('[chat-run-socket] queued run for session %s (queue: %d)', data.session_id, state.queue.length) return } + state.events = [] state.isWorking = true state.profile = runProfile state.source = source diff --git a/packages/skills/apikey-image-gen/SKILL.md b/packages/skills/apikey-image-gen/SKILL.md index 8d53a84..856ad8a 100644 --- a/packages/skills/apikey-image-gen/SKILL.md +++ b/packages/skills/apikey-image-gen/SKILL.md @@ -1,6 +1,6 @@ --- name: apikey-image-gen -description: "Generate or edit images through Hermes Web UI using the active profile's fun-codex provider from config.yaml." +description: "Generate or edit images through Hermes Web UI using the selected/requested profile's fun-codex provider from config.yaml." version: 1.0.0 author: Ekko license: MIT @@ -16,7 +16,9 @@ prerequisites: Use this skill when the user wants to generate an image, generate an image from a reference image, or edit an existing image. -Always call Hermes Web UI's media endpoint. Do not call `api.apikey.fun` directly, and do not ask the user for an API key. The server reads the active profile's `config.yaml` and uses the `custom_providers` entry named `fun-codex`: +Always call Hermes Web UI's media endpoint. Do not call `api.apikey.fun` directly, and do not ask the user for an API key. The server reads the selected/requested profile's `config.yaml` and uses the `custom_providers` entry named `fun-codex`: + +Do not use any built-in image generation tool as a fallback. If the Hermes Web UI endpoint returns `401`, `403`, connection failure, or any other error, stop and report the Hermes Web UI error to the user. ```yaml custom_providers: @@ -43,7 +45,7 @@ When Hermes Web UI is running from Docker Compose, the default external URL is ` Authentication: -Send the Hermes Web UI bearer token. +Send the Hermes Web UI server bearer token. This token is accepted only by Hermes Web UI media generation endpoints for agent skills; it is not a general Web UI login token. Resolve the token in this order: @@ -52,6 +54,20 @@ Resolve the token in this order: 3. `${HERMES_WEBUI_STATE_DIR}/.token`, if `HERMES_WEBUI_STATE_DIR` is set. 4. `~/.hermes-web-ui/.token`. +Profile selection: + +Use the current Hermes profile from the run instructions by sending `X-Hermes-Profile`. + +If the run instructions include `[Current Hermes profile: ]`, include: + +```bash +-H "X-Hermes-Profile: " +``` + +Replace `` with the exact profile name from the run instructions. Never send a placeholder value such as `` or ``. + +If no current profile is provided, omit the header and let the server fall back to the current Hermes active profile. + ## Modes ### Text To Image @@ -163,4 +179,4 @@ Successful responses include: } ``` -If the response code is `missing_fun_codex_provider`, tell the user to configure `fun-codex` in the active profile's `config.yaml`. +If the response code is `missing_fun_codex_provider`, tell the user to configure `fun-codex` in the selected/requested profile's `config.yaml`. diff --git a/packages/skills/grok-image-to-video/SKILL.md b/packages/skills/grok-image-to-video/SKILL.md index c5b367b..5b55ed6 100644 --- a/packages/skills/grok-image-to-video/SKILL.md +++ b/packages/skills/grok-image-to-video/SKILL.md @@ -16,6 +16,8 @@ prerequisites: Use this skill when the user wants to animate a local image into a short video with xAI Grok Imagine. +Do not use any built-in image or video generation tool as a fallback. If the Hermes Web UI endpoint returns `401`, `403`, connection failure, or any other error, stop and report the Hermes Web UI error to the user. + ## Workflow Call the local Hermes Web UI media endpoint. Pass a local image path; the server will check for xAI credentials, read the file, convert it to a base64 data URI, call xAI, poll until completion, and optionally save the generated mp4. @@ -36,7 +38,7 @@ When Hermes Web UI is running from the provided Docker Compose setup, the defaul Authentication: -The endpoint is protected by Hermes Web UI auth. Always send the Web UI bearer token. +The endpoint is protected by Hermes Web UI auth. Always send the Hermes Web UI server bearer token. This token is accepted only by Hermes Web UI media generation endpoints for agent skills; it is not a general Web UI login token. Resolve the token in this order: @@ -45,6 +47,20 @@ Resolve the token in this order: 3. `${HERMES_WEBUI_STATE_DIR}/.token`, if `HERMES_WEBUI_STATE_DIR` is set. 4. `~/.hermes-web-ui/.token`. +Profile selection: + +Use the current Hermes profile from the run instructions by sending `X-Hermes-Profile`. + +If the run instructions include `[Current Hermes profile: ]`, include: + +```bash +-H "X-Hermes-Profile: " +``` + +Replace `` with the exact profile name from the run instructions. Never send a placeholder value such as `` or ``. + +If no current profile is provided, omit the header and let the server fall back to the current Hermes active profile. + Required JSON fields: - `image_path`: local path to a png, jpeg, or webp image. diff --git a/packages/website/src/i18n/en.ts b/packages/website/src/i18n/en.ts index 923dd4d..4619e10 100644 --- a/packages/website/src/i18n/en.ts +++ b/packages/website/src/i18n/en.ts @@ -40,11 +40,11 @@ export default { }, profiles: { title: 'Multi-Profile', - desc: 'Isolated profiles with independent configs. Clone, import/export profiles, and run chats through the agent bridge.', + desc: 'Account-authorized Hermes profiles with isolated config, models, uploads, jobs, usage, memory, skills, plugins, and providers.', }, files: { title: 'File Browser', - desc: 'Manage files across local, Docker, SSH, and Singularity backends with upload, preview, and edit.', + desc: 'Manage files across local, Docker, SSH, and Singularity backends with profile-scoped upload plus path-based download, preview, and edit.', }, terminal: { title: 'Web Terminal', @@ -124,7 +124,7 @@ export default { }, login: { title: 'Login', - content: 'The auto-generated token is stored in ~/.hermes-web-ui/.token. You can also set up username/password login from the Settings page after your first login.', + content: 'The auto-generated token is stored in ~/.hermes-web-ui/.token. Username/password login is available with bootstrap credentials admin / 123456 on first use, and the app prompts users to change default credentials after login.', }, }, configuration: { @@ -137,18 +137,18 @@ export default { ['AUTH_TOKEN', 'Custom auth token (overrides auto-generated)'], ['PORT', 'Server listen port (default: 8648)'], ['BIND_HOST', 'Server bind host (default: 0.0.0.0). Set :: explicitly to enable IPv6 listening.'], - ['UPLOAD_DIR', 'Custom upload directory path'], + ['UPLOAD_DIR', 'Custom upload root. Uploaded files are stored below profile-scoped subdirectories.'], ['CORS_ORIGINS', 'CORS origin config (default: *)'], ['HERMES_BIN', 'Custom path to hermes CLI binary'], ], }, gateway: { title: 'Agent Bridge Runtime', - content: 'Chat runs are handled through the Hermes agent bridge, which runs alongside the Web UI server and talks directly to the Hermes Agent runtime. The Web UI no longer starts or manages separate gateway processes.', + content: 'Chat runs are handled through the Hermes agent bridge, which runs alongside the Web UI server and talks directly to the Hermes Agent runtime. Switching the frontend Hermes Profile changes later request context only; it does not restart the bridge or clear other running tasks.', }, profiles: { title: 'Profiles', - content: 'Profiles provide isolated configurations for different use cases. Each profile has its own Hermes config and cache. Create, clone, import, or export profiles from the Profiles page.', + content: 'Profiles provide isolated configurations for different use cases. Super administrators can manage every profile, while regular administrators only see and use profiles assigned to their account. Create, clone, import, export, or switch Hermes profiles from the Profiles page.', }, }, features: { @@ -156,7 +156,7 @@ export default { intro: 'Explore the core features of Hermes Web UI.', chat: { title: 'AI Chat', - content: 'Real-time chat streaming over Socket.IO /chat-run. Supports multi-session management, Markdown rendering with syntax highlighting, tool call inspection, file upload/download, and Ctrl+K search across the Web UI local session database.', + content: 'Real-time chat streaming over Socket.IO /chat-run. Supports multi-session management, Markdown rendering with syntax highlighting, tool call inspection, profile-scoped upload, path-based download, and Ctrl+K search across the Web UI local session database.', }, kanban: { title: 'Kanban Board', @@ -184,7 +184,7 @@ export default { }, files: { title: 'File Browser', - content: 'Browse and manage files on remote backends including local, Docker, SSH, and Singularity. Upload, download, rename, move, delete files, and preview content with syntax highlighting.', + content: 'Browse and manage files on remote backends including local, Docker, SSH, and Singularity. Uploads are stored under the selected/requested profile, while downloads resolve real paths so agent-generated artifacts outside the upload directory still work.', }, analytics: { title: 'Usage Analytics', @@ -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, config read/write, log access, skill listing, memory operations, and static assets.', + content: 'The Koa server handles session management, profile CRUD, account/profile authorization, config read/write, log access, skill listing, memory operations, and static assets.', }, proxy: { title: 'Chat Streaming', @@ -240,7 +240,7 @@ export default { }, auth: { title: 'Authentication', - content: 'All API endpoints require a Bearer token via the Authorization header. The token is auto-generated on first run and stored in ~/.hermes-web-ui/.token. Optional username/password login can be configured from the Settings page.', + content: 'API endpoints require authenticated access. The token is auto-generated on first run and stored in ~/.hermes-web-ui/.token. Username/password login uses account records; super administrators manage users and profile bindings, while regular administrators manage their own account details.', }, }, }, diff --git a/packages/website/src/i18n/zh.ts b/packages/website/src/i18n/zh.ts index 4209674..b0f18e9 100644 --- a/packages/website/src/i18n/zh.ts +++ b/packages/website/src/i18n/zh.ts @@ -40,11 +40,11 @@ export default { }, profiles: { title: '多配置', - desc: '隔离的多配置文件,独立配置。支持克隆、导入/导出,并通过 agent bridge 运行聊天。', + desc: '按账号授权的 Hermes Profile,隔离配置、模型、上传、任务、用量、记忆、技能、插件和 Provider。', }, files: { title: '文件管理', - desc: '跨本地、Docker、SSH 和 Singularity 管理文件,支持上传、预览和编辑。', + desc: '跨本地、Docker、SSH 和 Singularity 管理文件,支持按 Profile 上传、按路径下载、预览和编辑。', }, terminal: { title: 'Web 终端', @@ -124,7 +124,7 @@ export default { }, login: { title: '登录', - content: '自动生成的令牌存储在 ~/.hermes-web-ui/.token。首次登录后可在设置页面配置用户名/密码登录。', + content: '自动生成的令牌存储在 ~/.hermes-web-ui/.token。首次使用可通过默认登录名 admin / 默认密码 123456 登录;登录后系统会提示尽快修改默认账户和密码。', }, }, configuration: { @@ -137,18 +137,18 @@ export default { ['AUTH_TOKEN', '自定义认证令牌(覆盖自动生成的令牌)'], ['PORT', '服务器监听端口(默认:8648)'], ['BIND_HOST', '服务器绑定地址(默认:0.0.0.0)。如需 IPv6,请显式设置为 ::。'], - ['UPLOAD_DIR', '自定义上传目录路径'], + ['UPLOAD_DIR', '自定义上传根目录。文件会保存在按 Profile 隔离的子目录下'], ['CORS_ORIGINS', 'CORS 来源配置(默认:*)'], ['HERMES_BIN', '自定义 hermes CLI 二进制路径'], ], }, gateway: { title: 'Agent Bridge 运行时', - content: '聊天运行通过 Hermes agent bridge 处理。它随 Web UI 服务一起运行,并直接连接 Hermes Agent runtime。Web UI 不再启动或管理独立的 gateway 进程。', + content: '聊天运行通过 Hermes agent bridge 处理。它随 Web UI 服务一起运行,并直接连接 Hermes Agent runtime。前端切换 Hermes Profile 只影响后续请求上下文,不会重启 bridge 或清理其他正在运行的任务。', }, profiles: { title: '配置文件', - content: '配置文件为不同场景提供隔离的配置。每个配置文件拥有独立的 Hermes 配置和缓存。可在配置页面创建、克隆、导入或导出配置文件。', + content: 'Profile 为不同场景提供隔离配置。超级管理员可以管理全部 Profile;普通管理员只能查看和使用分配给自己的 Profile。可在 Profile 页面创建、克隆、导入、导出或切换 Hermes Profile。', }, }, features: { @@ -156,7 +156,7 @@ export default { intro: '探索 Hermes Web UI 的核心功能。', chat: { title: 'AI 聊天', - content: '通过 Socket.IO /chat-run 实时流式聊天。支持多会话管理、Markdown 渲染与语法高亮、工具调用检查、文件上传/下载,以及 Ctrl+K 搜索 Web UI 本地会话库。', + content: '通过 Socket.IO /chat-run 实时流式聊天。支持多会话管理、Markdown 渲染与语法高亮、工具调用检查、按 Profile 上传、按路径下载,以及 Ctrl+K 搜索 Web UI 本地会话库。', }, kanban: { title: '看板管理', @@ -184,7 +184,7 @@ export default { }, files: { title: '文件管理', - content: '浏览和管理本地、Docker、SSH 和 Singularity 等远程后端上的文件。支持上传、下载、重命名、移动、删除文件以及带语法高亮的内容预览。', + content: '浏览和管理本地、Docker、SSH 和 Singularity 等远程后端上的文件。上传保存到当前选择/请求的 Profile;下载按真实路径解析,因此上传目录外的 Agent 产物也可以下载。', }, analytics: { title: '用量分析', @@ -232,7 +232,7 @@ export default { intro: 'Hermes Web UI 提供本地 BFF API,并通过 Socket.IO 端点进行聊天流式通信。', local: { title: '本地 BFF 端点', - content: 'Koa 服务器处理会话管理、配置文件 CRUD、配置读写、日志访问、技能列表、记忆操作和静态资源。', + content: 'Koa 服务器处理会话管理、Profile CRUD、账号/Profile 鉴权、配置读写、日志访问、技能列表、记忆操作和静态资源。', }, proxy: { title: '聊天流式通信', @@ -240,7 +240,7 @@ export default { }, auth: { title: '认证', - content: '所有 API 端点需要通过 Authorization 头提供 Bearer 令牌。令牌在首次运行时自动生成并存储在 ~/.hermes-web-ui/.token。可在设置页面配置可选的用户名/密码登录。', + content: 'API 端点需要经过认证访问。令牌在首次运行时自动生成并存储在 ~/.hermes-web-ui/.token。用户名/密码登录使用账户记录;超级管理员管理用户和 Profile 绑定,普通管理员管理自己的账户信息。', }, }, }, diff --git a/tests/server/run-chat-bridge-final-context.test.ts b/tests/server/run-chat-bridge-final-context.test.ts index b06db80..e7e3ae0 100644 --- a/tests/server/run-chat-bridge-final-context.test.ts +++ b/tests/server/run-chat-bridge-final-context.test.ts @@ -169,10 +169,12 @@ describe('bridge run final context usage', () => { { role: 'user', content: 'hello' }, { role: 'assistant', content: 'done' }, ], - 'system prompt', + expect.stringContaining('[Current Hermes profile: default]'), 'default', { model: 'gpt-test', provider: 'openai' }, ) + expect(bridge.contextEstimate.mock.calls[0][2]).toContain('system prompt') + expect(bridge.contextEstimate.mock.calls[0][2]).toContain('X-Hermes-Profile') expect(state.contextTokens).toBe(12345) expect(emit).toHaveBeenCalledWith('usage.updated', expect.objectContaining({ inputTokens: 11,