feat: profile-aware routes, provider sync, channel settings improvements
- Add hermes-profile.ts for dynamic profile path resolution (all backend routes now read from active profile directory instead of hardcoded ~/.hermes/) - Add profile switcher dropdown in sidebar, reload page on switch - Sync PROVIDER_PRESETS with Hermes CLI (fix keys: kimi-coding→kimi-for-coding, kilocode→kilo, ai-gateway→vercel, opencode-zen→opencode; remove moonshot) - Sync PROVIDER_ENV_MAP with Hermes models.dev + overlays (correct env var names) - Add gateway restart after adding model provider - Don't write GLM_BASE_URL/KIMI_BASE_URL for zai/kimi (let Hermes auto-detect) - Write API keys to .env and credential_pool for all providers - Built-in providers skip custom_providers in config.yaml - Add debounce + per-field loading state for channel settings inputs - Run hermes setup --reset for profiles without config.yaml - Create empty .env for new profiles (not copied from default) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,40 @@
|
||||
import Router from '@koa/router'
|
||||
import { createReadStream, existsSync, unlinkSync } from 'fs'
|
||||
import { createReadStream, existsSync, unlinkSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs'
|
||||
import { basename, join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { tmpdir, homedir } from 'os'
|
||||
import YAML from 'js-yaml'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
|
||||
const apiServerDefaults = {
|
||||
enabled: true,
|
||||
host: '127.0.0.1',
|
||||
port: 8642,
|
||||
key: '',
|
||||
cors_origins: '*',
|
||||
}
|
||||
|
||||
function ensureApiServerConfig(profilePath: string) {
|
||||
const configPath = join(profilePath, 'config.yaml')
|
||||
try {
|
||||
if (!existsSync(configPath)) {
|
||||
// Profile has no config.yaml — run hermes setup --reset to generate full defaults,
|
||||
// then inject api_server config (setup itself doesn't add it)
|
||||
console.log(`[Profile] No config.yaml for ${profilePath}, running setup --reset`)
|
||||
return { needSetup: true, path: profilePath }
|
||||
}
|
||||
const content = readFileSync(configPath, 'utf-8')
|
||||
const cfg = YAML.load(content) as any || {}
|
||||
if (!cfg.platforms) cfg.platforms = {}
|
||||
if (!cfg.platforms.api_server) {
|
||||
cfg.platforms.api_server = { ...apiServerDefaults }
|
||||
writeFileSync(configPath, YAML.dump(cfg), 'utf-8')
|
||||
console.log(`[Profile] Ensured api_server config for: ${profilePath}`)
|
||||
}
|
||||
return { needSetup: false, path: profilePath }
|
||||
} catch { }
|
||||
return { needSetup: false, path: profilePath }
|
||||
}
|
||||
|
||||
export const profileRoutes = new Router()
|
||||
|
||||
// GET /api/profiles - List all profiles
|
||||
@@ -109,10 +140,10 @@ profileRoutes.put('/api/hermes/profiles/active', async (ctx) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Stop gateway (try launchd/systemd first, ignore if unavailable e.g. WSL)
|
||||
// 1. Stop gateway
|
||||
try { await hermesCli.stopGateway() } catch { }
|
||||
|
||||
// 2. Kill gateway by port if still running (for WSL / background mode)
|
||||
// 2. Kill gateway by port if still running
|
||||
try {
|
||||
const { execSync } = await import('child_process')
|
||||
const isWin = process.platform === 'win32'
|
||||
@@ -138,11 +169,34 @@ profileRoutes.put('/api/hermes/profiles/active', async (ctx) => {
|
||||
const output = await hermesCli.useProfile(name)
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
|
||||
// 4. Start gateway — try launchd/systemd first, fall back to background mode
|
||||
// 4. Ensure api_server config for new profile
|
||||
try {
|
||||
await hermesCli.restartGateway()
|
||||
const detail = await hermesCli.getProfile(name)
|
||||
console.log(`[Profile] detail.path = ${detail.path}`)
|
||||
const result = ensureApiServerConfig(detail.path)
|
||||
if (result?.needSetup) {
|
||||
// No config.yaml — run setup --reset to create full default config,
|
||||
// then ensure api_server is present
|
||||
try { await hermesCli.setupReset() } catch { }
|
||||
ensureApiServerConfig(detail.path)
|
||||
}
|
||||
// Create .env if target has none
|
||||
const profileEnv = join(detail.path, '.env')
|
||||
console.log(`[Profile] .env exists: ${existsSync(profileEnv)}, path: ${profileEnv}`)
|
||||
if (!existsSync(profileEnv)) {
|
||||
writeFileSync(profileEnv, '# Hermes Agent Environment Configuration\n', 'utf-8')
|
||||
console.log(`[Profile] Created .env for: ${detail.path}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`[Profile] Ensure config failed:`, err.message)
|
||||
}
|
||||
|
||||
// 5. Start gateway
|
||||
try {
|
||||
await hermesCli.startGateway()
|
||||
console.log('[Profile] Gateway started')
|
||||
} catch {
|
||||
// Fallback for WSL / environments without launchd/systemd
|
||||
// Fallback: background mode (for WSL etc.)
|
||||
try {
|
||||
const pid = await hermesCli.startGatewayBackground()
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
Reference in New Issue
Block a user