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
@@ -2,12 +2,15 @@
import { computed, ref, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useMessage } from "naive-ui";
import { useAppStore } from "@/stores/hermes/app";
import ModelSelector from "./ModelSelector.vue";
import ProfileSelector from "./ProfileSelector.vue";
import LanguageSwitch from "./LanguageSwitch.vue";
import danceVideo from "@/assets/dance.mp4";
const { t } = useI18n();
const message = useMessage();
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
@@ -63,6 +66,15 @@ onMounted(() => {
function handleNav(key: string) {
router.push({ name: key });
}
async function handleUpdate() {
const ok = await appStore.doUpdate();
if (ok) {
message.success(t('sidebar.updateSuccess'), { duration: 5000 });
} else {
message.error(t('sidebar.updateFailed'));
}
}
</script>
<template>
@@ -147,27 +159,6 @@ function handleNav(key: string) {
<span>{{ t("sidebar.models") }}</span>
</button>
<button
class="nav-item"
:class="{ active: selectedKey === 'hermes.profiles' }"
@click="handleNav('hermes.profiles')"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span>{{ t("sidebar.profiles") }}</span>
</button>
<button
class="nav-item"
:class="{ active: selectedKey === 'hermes.channels' }"
@@ -280,6 +271,27 @@ function handleNav(key: string) {
<span>{{ t("sidebar.usage") }}</span>
</button>
<button
class="nav-item"
:class="{ active: selectedKey === 'hermes.profiles' }"
@click="handleNav('hermes.profiles')"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
<span>{{ t("sidebar.profiles") }}</span>
</button>
<button
class="nav-item"
:class="{ active: selectedKey === 'hermes.terminal' }"
@@ -325,6 +337,7 @@ function handleNav(key: string) {
</button>
</nav>
<ProfileSelector />
<ModelSelector />
<div class="sidebar-footer">
@@ -346,7 +359,10 @@ function handleNav(key: string) {
<LanguageSwitch />
</div>
<div class="version-info">
Hermes {{ appStore.serverVersion || "v0.1.0" }}
<span>Hermes Web UI v{{ appStore.serverVersion || "0.1.0" }}</span>
<a v-if="appStore.updateAvailable" class="update-hint" :class="{ loading: appStore.updating }" @click="handleUpdate">
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
</a>
</div>
</div>
</aside>
@@ -483,6 +499,31 @@ function handleNav(key: string) {
padding: 2px 12px 8px;
font-size: 11px;
color: $text-muted;
display: flex;
flex-direction: column;
gap: 2px;
}
.update-hint {
display: block;
margin-top: 4px;
padding: 5px 10px;
border-radius: $radius-sm;
background: #333333;
color: rgba(#fff, 0.7);
font-size: 11px;
text-align: center;
cursor: pointer;
transition: background $transition-fast;
&:hover {
background: #3d3d3d;
}
&.loading {
pointer-events: none;
opacity: 0.7;
}
}
@media (max-width: $breakpoint-mobile) {