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:
ekko
2026-04-16 13:51:42 +08:00
parent 014168864f
commit 99a47cf1ad
23 changed files with 712 additions and 185 deletions
+61 -7
View File
@@ -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))