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
@@ -9,7 +9,7 @@ const { t } = useI18n()
const message = useMessage()
const loading = ref(false)
const data = ref<MemoryData | null>(null)
const editingSection = ref<'memory' | 'user' | null>(null)
const editingSection = ref<'memory' | 'user' | 'soul' | null>(null)
const editContent = ref('')
const saving = ref(false)
@@ -27,7 +27,7 @@ async function loadMemory() {
}
}
function startEdit(section: 'memory' | 'user') {
function startEdit(section: 'memory' | 'user' | 'soul') {
editingSection.value = section
editContent.value = data.value?.[section] || ''
}
@@ -65,9 +65,11 @@ function formatTime(ts: number | null): string {
const memoryEmpty = computed(() => !data.value?.memory?.trim())
const userEmpty = computed(() => !data.value?.user?.trim())
const soulEmpty = computed(() => !data.value?.soul?.trim())
const displayMemory = computed(() => (data.value?.memory || '').replace(/§/g, '\n\n'))
const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n'))
const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n'))
</script>
<template>
@@ -179,6 +181,53 @@ const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n
</div>
</div>
</div>
<!-- Soul -->
<div class="memory-section">
<div class="section-header">
<div class="section-title-row">
<span class="section-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
<line x1="9" y1="9" x2="9.01" y2="9" />
<line x1="15" y1="9" x2="15.01" y2="9" />
</svg>
</span>
<span class="section-title">{{ t('memory.soul') }}</span>
<span v-if="data?.soul_mtime" class="section-mtime">{{ formatTime(data.soul_mtime) }}</span>
</div>
<NButton v-if="editingSection !== 'soul'" size="tiny" quaternary @click="startEdit('soul')">
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</template>
{{ t('common.edit') }}
</NButton>
</div>
<!-- View mode -->
<div v-if="editingSection !== 'soul'" class="section-body">
<MarkdownRenderer v-if="!soulEmpty" :content="displaySoul" />
<p v-else class="empty-text">{{ t('memory.noSoul') }}</p>
</div>
<!-- Edit mode -->
<div v-else class="section-edit">
<textarea
v-model="editContent"
class="edit-textarea"
:placeholder="t('memory.soulPlaceholder')"
spellcheck="false"
></textarea>
<div class="edit-actions">
<NButton size="small" @click="cancelEdit">{{ t('common.cancel') }}</NButton>
<NButton size="small" type="primary" :loading="saving" @click="handleSave">{{ t('common.save') }}</NButton>
</div>
</div>
</div>
</div>
</div>
</div>