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
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { NSelect } from 'naive-ui'
import { useProfilesStore } from '@/stores/hermes/profiles'
import { useAppStore } from '@/stores/hermes/app'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const profilesStore = useProfilesStore()
const appStore = useAppStore()
const options = computed(() =>
profilesStore.profiles.map(p => ({
label: p.name,
value: p.name,
})),
)
const activeName = computed(() => profilesStore.activeProfile?.name ?? '')
function handleChange(value: string | number | Array<string | number>) {
if (typeof value === 'string' && value !== activeName.value) {
profilesStore.switchProfile(value).then(ok => {
if (ok) {
window.location.reload()
}
})
}
}
onMounted(() => {
if (profilesStore.profiles.length === 0) {
profilesStore.fetchProfiles()
}
})
</script>
<template>
<div class="profile-selector">
<div class="selector-label">{{ t('sidebar.profiles') }}</div>
<NSelect
:value="activeName"
:options="options"
:loading="profilesStore.switching"
size="small"
@update:value="handleChange"
/>
</div>
</template>
<style scoped lang="scss">
@use '@/styles/variables' as *;
.profile-selector {
padding: 0 12px;
margin-bottom: 8px;
}
.selector-label {
font-size: 11px;
font-weight: 600;
color: $text-muted;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
</style>