make web ui state directory configurable (#764)
This commit is contained in:
@@ -205,6 +205,28 @@ Open **http://localhost:6060**
|
||||
|
||||
For detailed notes and troubleshooting, see [`docs/docker.md`](./docs/docker.md).
|
||||
|
||||
## Web UI Environment Variables
|
||||
|
||||
These variables configure Hermes Web UI itself. Provider API keys and Hermes Agent settings are managed separately through Hermes profiles.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `LOG_LEVEL` | `info` | Server log level. |
|
||||
| `BRIDGE_LOG_LEVEL` | `$LOG_LEVEL` or `info` | Bridge log level. |
|
||||
| `MAX_DOWNLOAD_SIZE` | `200MB` | Maximum file download size. |
|
||||
| `MAX_EDIT_SIZE` | `10MB` | Maximum editable file size. |
|
||||
| `WORKSPACE_BASE` | `/opt/data/workspace` | Base directory for workspace browsing. |
|
||||
| `GATEWAY_HOST` | `127.0.0.1` | Default gateway host written into profile config. |
|
||||
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | environment-dependent | Whether Web UI shutdown also stops managed gateways. |
|
||||
|
||||
### CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
|
||||
@@ -213,6 +213,28 @@ docker compose logs -f hermes-webui
|
||||
|
||||
更详细的说明与排错见:[`docs/docker.md`](./docs/docker.md)
|
||||
|
||||
## Web UI 环境变量
|
||||
|
||||
这些变量只用于配置 Hermes Web UI 自身。Provider API Key 和 Hermes Agent 相关设置仍通过 Hermes profile 管理。
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `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` | 覆盖上传目录。 |
|
||||
| `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 名称。 |
|
||||
| `LOG_LEVEL` | `info` | Server 日志级别。 |
|
||||
| `BRIDGE_LOG_LEVEL` | `$LOG_LEVEL` 或 `info` | Bridge 日志级别。 |
|
||||
| `MAX_DOWNLOAD_SIZE` | `200MB` | 最大文件下载大小。 |
|
||||
| `MAX_EDIT_SIZE` | `10MB` | 最大可编辑文件大小。 |
|
||||
| `WORKSPACE_BASE` | `/opt/data/workspace` | Workspace 浏览根目录。 |
|
||||
| `GATEWAY_HOST` | `127.0.0.1` | 写入 profile config 的默认 gateway host。 |
|
||||
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | 视环境而定 | Web UI 关闭时是否同时停止托管的 gateways。 |
|
||||
|
||||
### CLI 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|
||||
@@ -11,7 +11,10 @@ const serverEntry = resolve(__dirname, '..', 'dist', 'server', 'index.js')
|
||||
const pkgDir = resolve(__dirname, '..')
|
||||
const pkg = JSON.parse(readFileSync(resolve(pkgDir, 'package.json'), 'utf-8'))
|
||||
const VERSION = pkg.version
|
||||
const PID_DIR = resolve(homedir(), '.hermes-web-ui')
|
||||
const WEB_UI_HOME = process.env.HERMES_WEB_UI_HOME?.trim()
|
||||
? resolve(process.env.HERMES_WEB_UI_HOME.trim())
|
||||
: resolve(homedir(), '.hermes-web-ui')
|
||||
const PID_DIR = WEB_UI_HOME
|
||||
const PID_FILE = join(PID_DIR, 'server.pid')
|
||||
const LOG_FILE = join(PID_DIR, 'server.log')
|
||||
const TOKEN_FILE = join(PID_DIR, '.token')
|
||||
|
||||
@@ -1,18 +1,55 @@
|
||||
import { resolve } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
/**
|
||||
* Web UI environment variables.
|
||||
*
|
||||
* Server/listen:
|
||||
* - PORT: Web UI listen port. Default: 8648.
|
||||
* - BIND_HOST: Web UI bind host. Default: 0.0.0.0.
|
||||
* - CORS_ORIGINS: Koa CORS origin setting. Default: *.
|
||||
*
|
||||
* Web UI storage:
|
||||
* - HERMES_WEB_UI_HOME: Web UI data home for auth token, credentials, logs, DB, and default uploads.
|
||||
* - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME.
|
||||
* Default: join(homedir(), '.hermes-web-ui').
|
||||
* - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload').
|
||||
*
|
||||
* Auth:
|
||||
* - AUTH_DISABLED: Set to 1 or true to disable Web UI auth.
|
||||
* - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME.
|
||||
*
|
||||
* Runtime behavior:
|
||||
* - PROFILE: Initial Hermes profile name. Default: default.
|
||||
* - GATEWAY_HOST: Default gateway host written into profile config. Default: 127.0.0.1.
|
||||
* - HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN: Whether Web UI shutdown also stops gateways.
|
||||
* - WORKSPACE_BASE: Base directory for workspace browsing. Default: /opt/data/workspace.
|
||||
*
|
||||
* Limits/logging:
|
||||
* - MAX_DOWNLOAD_SIZE: Max file download size. Default: 200MB.
|
||||
* - MAX_EDIT_SIZE: Max editable file size. Default: 10MB.
|
||||
* - LOG_LEVEL: Server log level. Default: info.
|
||||
* - BRIDGE_LOG_LEVEL: Bridge log level. Default: LOG_LEVEL or info.
|
||||
*/
|
||||
|
||||
export function getListenHost(env: Record<string, string | undefined> = process.env): string {
|
||||
const host = env.BIND_HOST?.trim()
|
||||
return host || '0.0.0.0'
|
||||
}
|
||||
|
||||
export function getWebUiHome(env: Record<string, string | undefined> = process.env): string {
|
||||
const appHome = env.HERMES_WEB_UI_HOME?.trim() || env.HERMES_WEBUI_STATE_DIR?.trim()
|
||||
return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui')
|
||||
}
|
||||
|
||||
const appHome = getWebUiHome()
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '8648', 10),
|
||||
// Default to IPv4 for stable WSL/Windows browser access. Use BIND_HOST=:: explicitly for IPv6.
|
||||
host: getListenHost(),
|
||||
uploadDir: process.env.UPLOAD_DIR || resolve(homedir(), '.hermes-web-ui', 'upload'),
|
||||
appHome,
|
||||
uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'),
|
||||
dataDir: resolve(__dirname, '..', 'data'),
|
||||
corsOrigins: process.env.CORS_ORIGINS || '*',
|
||||
/** Session store: 'local' (self-built SQLite) or 'remote' (Hermes CLI) */
|
||||
sessionStore: (process.env.SESSION_STORE || 'local') as 'local' | 'remote',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { existsSync, statSync } from 'fs'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
import { config } from '../../config'
|
||||
|
||||
const WEBUI_LOG_FILE = join(homedir(), '.hermes-web-ui', 'logs', 'server.log')
|
||||
const BRIDGE_LOG_FILE = join(homedir(), '.hermes-web-ui', 'logs', 'bridge.log')
|
||||
const WEBUI_LOG_FILE = join(config.appHome, 'logs', 'server.log')
|
||||
const BRIDGE_LOG_FILE = join(config.appHome, 'logs', 'bridge.log')
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: string; level: string; logger: string; message: string; raw: string
|
||||
|
||||
@@ -469,11 +469,3 @@ export function getSessionDetailPaginated(
|
||||
hasMore: offset + messages.length < total,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Session store mode ---
|
||||
|
||||
import { config } from '../../config'
|
||||
|
||||
export function useLocalSessionStore(): boolean {
|
||||
return config.sessionStore === 'local'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import { config } from '../config'
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production'
|
||||
const isTest = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'
|
||||
@@ -11,7 +11,7 @@ const DB_DIR = isTest
|
||||
? resolve(process.cwd(), 'packages/server/data/test-runtime')
|
||||
: isDev
|
||||
? resolve(process.cwd(), 'packages/server/data')
|
||||
: resolve(homedir(), '.hermes-web-ui')
|
||||
: config.appHome
|
||||
const DB_PATH = resolve(DB_DIR, 'hermes-web-ui.db')
|
||||
const JSON_PATH = resolve(DB_DIR, 'hermes-web-ui.json')
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ export async function bootstrap() {
|
||||
const interfaces = safeNetworkInterfaces()
|
||||
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
||||
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
||||
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
||||
console.log(`Log: ${config.appHome}/logs/server.log`)
|
||||
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
||||
|
||||
// Restore group chat agents after server is ready.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import { config } from '../config'
|
||||
|
||||
const APP_HOME = join(homedir(), '.hermes-web-ui')
|
||||
const APP_HOME = config.appHome
|
||||
const APP_CONFIG_FILE = join(APP_HOME, 'config.json')
|
||||
|
||||
export interface ModelVisibilityRule {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { homedir } from 'os'
|
||||
import { checkToken, recordTokenFailure, extractIp } from './login-limiter'
|
||||
import { config } from '../config'
|
||||
|
||||
const APP_HOME = join(homedir(), '.hermes-web-ui')
|
||||
const APP_HOME = config.appHome
|
||||
const TOKEN_FILE = join(APP_HOME, '.token')
|
||||
|
||||
function generateToken(): string {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { readFile, writeFile, mkdir, unlink } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import { scryptSync, randomBytes } from 'node:crypto'
|
||||
import { config } from '../config'
|
||||
|
||||
const APP_HOME = join(homedir(), '.hermes-web-ui')
|
||||
const APP_HOME = config.appHome
|
||||
const CREDENTIALS_FILE = join(APP_HOME, '.credentials')
|
||||
|
||||
export interface Credentials {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
import pino from 'pino'
|
||||
import { resolve } from 'path'
|
||||
import { mkdirSync, statSync, truncateSync, openSync, readSync, closeSync, writeFileSync } from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { config } from '../config'
|
||||
|
||||
const MAX_LOG_SIZE = 3 * 1024 * 1024 // 3MB
|
||||
const CHECK_INTERVAL = 60_000 // Check every minute
|
||||
|
||||
const logDir = resolve(homedir(), '.hermes-web-ui', 'logs')
|
||||
const logDir = resolve(config.appHome, 'logs')
|
||||
mkdirSync(logDir, { recursive: true })
|
||||
|
||||
const logFile = resolve(logDir, 'server.log')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
import { config } from '../config'
|
||||
|
||||
const APP_HOME = join(homedir(), '.hermes-web-ui')
|
||||
const APP_HOME = config.appHome
|
||||
const LOCK_FILE = join(APP_HOME, '.login-lock.json')
|
||||
|
||||
// Per-IP settings
|
||||
|
||||
@@ -9,11 +9,6 @@ vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
|
||||
getSessionDetailPaginated: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
updateSessionStats: vi.fn(),
|
||||
useLocalSessionStore: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/db/hermes/sessions-db', () => ({
|
||||
getSessionDetailFromDb: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/logger', () => ({
|
||||
@@ -44,7 +39,7 @@ vi.mock('../../packages/server/src/db/hermes/usage-store', () => ({
|
||||
updateUsage: vi.fn(),
|
||||
}))
|
||||
|
||||
// --- Types mirroring chat-run-socket.ts ---
|
||||
// --- Types mirroring run-chat response flushing ---
|
||||
|
||||
interface SessionMessage {
|
||||
id: number | string
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getListenHost } from '../../packages/server/src/config'
|
||||
import { homedir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
import { getListenHost, getWebUiHome } from '../../packages/server/src/config'
|
||||
|
||||
describe('server config', () => {
|
||||
it('defaults to an IPv4 bind host', () => {
|
||||
@@ -13,4 +15,16 @@ describe('server config', () => {
|
||||
it('ignores blank BIND_HOST values', () => {
|
||||
expect(getListenHost({ BIND_HOST: ' ' })).toBe('0.0.0.0')
|
||||
})
|
||||
|
||||
it('defaults web-ui home to ~/.hermes-web-ui', () => {
|
||||
expect(getWebUiHome({})).toBe(join(homedir(), '.hermes-web-ui'))
|
||||
})
|
||||
|
||||
it('uses HERMES_WEB_UI_HOME when provided', () => {
|
||||
expect(getWebUiHome({ HERMES_WEB_UI_HOME: ' ./tmp/hermes-ui ' })).toBe(resolve('./tmp/hermes-ui'))
|
||||
})
|
||||
|
||||
it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => {
|
||||
expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,9 +49,7 @@ vi.mock('../../packages/server/src/db/hermes/sessions-db', () => ({
|
||||
getUsageStatsFromDb: getUsageStatsFromDbMock,
|
||||
}))
|
||||
|
||||
// Mock useLocalSessionStore to return false so we test the CLI path
|
||||
vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
|
||||
useLocalSessionStore: () => false,
|
||||
listSessions: localListSessionsMock,
|
||||
searchSessions: localSearchSessionsMock,
|
||||
getSessionDetail: localGetSessionDetailMock,
|
||||
|
||||
Reference in New Issue
Block a user