make web ui state directory configurable (#764)

This commit is contained in:
ekko
2026-05-15 17:30:27 +08:00
committed by GitHub
parent fbb0236af4
commit e0bfa828cb
17 changed files with 121 additions and 2228 deletions
+41 -4
View File
@@ -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'
}
+2 -2
View File
@@ -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')
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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