fix: generate token on start, include token in URL, reset api_server config

- Pre-generate auth token before server start and pass via AUTH_TOKEN env var
- Append token to startup URL for auto-login
- Reset api_server config values to defaults on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-15 08:28:36 +08:00
parent 1f45254dd0
commit 66cc9a6f1e
4 changed files with 24 additions and 69 deletions
-59
View File
@@ -1,59 +0,0 @@
# Official Hermes Dashboard API Integration
Branch: `feat/official-api`
## Overview
Integrate with the official Hermes REST API (`hermes dashboard` on `127.0.0.1:9119`) to replace or supplement CLI-based data fetching.
Reference: https://hermes-agent.nousresearch.com/docs/user-guide/features/web-dashboard
## Priority
### High
1. **Config management page**`GET/PUT /api/config`, `GET /api/config/schema`
- New settings page with form-based config editor
- All config fields auto-discovered from schema
- Save, reset to defaults, export/import
2. **API Key management**`GET/PUT/DELETE /api/env`
- View, set, delete API keys
- Grouped by category (LLM, Tools, Messaging)
- Redacted value display
3. **Session search**`GET /api/sessions/search?q=...`
- Full-text search across all message content
- Highlighted snippets
### Medium
4. **Analytics**`GET /api/analytics/usage?days=30`
- Use official API data instead of computing from session list
- More accurate cost/cache stats
5. **Cron job management** — Full CRUD
- Create, pause/resume, trigger, delete scheduled jobs
- Job list with status, schedule, run history
6. **Skills toggle**`PUT /api/skills/toggle`
- Enable/disable skills directly from UI
7. **Status enhancement**`GET /api/status`
- Platform connection states
- Active session count
### Low
8. **Toolsets**`GET /api/tools/toolsets`
- Display available toolsets with status
## Architecture
- BFF (Koa) proxies requests to official API at `127.0.0.1:9119`
- Fallback to CLI when official API is not available
- User can configure official dashboard address in settings
- CORS: official API restricts to localhost, our BFF handles this
## API Endpoints to Integrate
- `GET /api/status`
- `GET /api/sessions`, `GET /api/sessions/{id}`, `GET /api/sessions/{id}/messages`
- `GET /api/sessions/search?q=...`
- `DELETE /api/sessions/{id}`
- `GET /api/config`, `GET /api/config/defaults`, `GET /api/config/schema`, `PUT /api/config`
- `GET /api/env`, `PUT /api/env`, `DELETE /api/env`
- `GET /api/logs`
- `GET /api/analytics/usage?days=N`
- `GET /api/cron/jobs`, `POST /api/cron/jobs`, `POST/DELETE /api/cron/jobs/{id}/*`
- `GET /api/skills`, `PUT /api/skills/toggle`
- `GET /api/tools/toolsets`
+22 -8
View File
@@ -3,6 +3,7 @@ import { spawn, execSync } from 'child_process'
import { resolve, dirname, join } from 'path' import { resolve, dirname, join } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync } from 'fs' import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync } from 'fs'
import { randomBytes } from 'crypto'
import { homedir } from 'os' import { homedir } from 'os'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -23,6 +24,20 @@ function getToken() {
} }
} }
function ensureToken() {
// If AUTH_DISABLED or AUTH_TOKEN is set, let server handle it
if (process.env.AUTH_DISABLED === '1' || process.env.AUTH_DISABLED === 'true') return null
if (process.env.AUTH_TOKEN) return process.env.AUTH_TOKEN
let token = getToken()
if (!token) {
mkdirSync(dirname(TOKEN_FILE), { recursive: true })
token = randomBytes(32).toString('hex')
writeFileSync(TOKEN_FILE, token + '\n', { mode: 0o600 })
}
return token
}
function getPort() { function getPort() {
if (process.argv[3] && !isNaN(process.argv[3])) return parseInt(process.argv[3]) if (process.argv[3] && !isNaN(process.argv[3])) return parseInt(process.argv[3])
if (process.argv.includes('--port')) return parseInt(process.argv[process.argv.indexOf('--port') + 1]) if (process.argv.includes('--port')) return parseInt(process.argv[process.argv.indexOf('--port') + 1])
@@ -90,11 +105,13 @@ function startDaemon(port) {
mkdirSync(PID_DIR, { recursive: true }) mkdirSync(PID_DIR, { recursive: true })
const token = ensureToken()
const logStream = openSync(LOG_FILE, 'a') const logStream = openSync(LOG_FILE, 'a')
const child = spawn(process.execPath, [serverEntry], { const child = spawn(process.execPath, [serverEntry], {
detached: true, detached: true,
stdio: ['ignore', logStream, logStream], stdio: ['ignore', logStream, logStream],
env: { ...process.env, PORT: String(port) }, env: { ...process.env, PORT: String(port), AUTH_TOKEN: token },
windowsHide: true, windowsHide: true,
}) })
@@ -110,14 +127,11 @@ function startDaemon(port) {
setTimeout(() => { setTimeout(() => {
if (isRunning(child.pid)) { if (isRunning(child.pid)) {
console.log(` ✓ hermes-web-ui started (PID: ${child.pid}, port: ${port})`) console.log(` ✓ hermes-web-ui started (PID: ${child.pid}, port: ${port})`)
console.log(` http://localhost:${port}`) const url = token
? `http://localhost:${port}/#/?token=${token}`
: `http://localhost:${port}`
console.log(` ${url}`)
console.log(` Log: ${LOG_FILE}`) console.log(` Log: ${LOG_FILE}`)
const token = getToken()
if (token) {
console.log(` Token: ${token}`)
}
// Open browser
const url = `http://localhost:${port}`
const isWin = process.platform === 'win32' const isWin = process.platform === 'win32'
const cmd = isWin ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}` const cmd = isWin ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`
try { execSync(cmd, { stdio: 'ignore' }) } catch {} try { execSync(cmd, { stdio: 'ignore' }) } catch {}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hermes-web-ui", "name": "hermes-web-ui",
"version": "0.2.2", "version": "0.2.3",
"description": "Hermes Agent Web UI - Chat and Job Management Dashboard", "description": "Hermes Agent Web UI - Chat and Job Management Dashboard",
"repository": { "repository": {
"type": "git", "type": "git",
+1 -1
View File
@@ -196,7 +196,7 @@ async function ensureApiServerConfig() {
let changed = false let changed = false
for (const [k, v] of Object.entries(defaults)) { for (const [k, v] of Object.entries(defaults)) {
if (api[k] == null) { if (api[k] != null && api[k] !== v) {
api[k] = v api[k] = v
changed = true changed = true
} }