Merge branch 'dev' into main — v0.3.2
- fix: use deep merge for config updates to prevent nested field loss - fix: save config inputs on blur instead of every keystroke - fix: job edit schedule format error (#25) - chore: add engines requirement (node >= 20) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hermes-web-ui",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"description": "Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration (Telegram, Discord, Slack, WhatsApp)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,6 +8,9 @@
|
||||
},
|
||||
"homepage": "https://github.com/EKKOLearnAI/hermes-web-ui",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"hermes",
|
||||
"ai-agent",
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface CreateJobRequest {
|
||||
|
||||
export interface UpdateJobRequest {
|
||||
name?: string
|
||||
schedule?: string
|
||||
schedule?: string | { kind: string; expr: string; display: string }
|
||||
prompt?: string
|
||||
deliver?: string
|
||||
skills?: string[]
|
||||
|
||||
@@ -48,6 +48,8 @@ const targetOptions = computed(() => [
|
||||
{ label: t('jobs.local'), value: 'local' },
|
||||
])
|
||||
|
||||
const originalSchedule = ref<{ kind: string; expr: string; display: string } | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.jobId) {
|
||||
try {
|
||||
@@ -60,6 +62,9 @@ onMounted(async () => {
|
||||
deliver: job.deliver || 'origin',
|
||||
repeat_times: typeof job.repeat === 'number' ? job.repeat : (typeof job.repeat === 'object' ? job.repeat.times : null),
|
||||
}
|
||||
if (typeof job.schedule === 'object' && job.schedule) {
|
||||
originalSchedule.value = job.schedule
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(t('jobs.loadFailed') + ': ' + e.message)
|
||||
}
|
||||
@@ -86,6 +91,14 @@ async function handleSave() {
|
||||
repeat: formData.value.repeat_times ?? undefined,
|
||||
}
|
||||
|
||||
if (isEdit.value && originalSchedule.value) {
|
||||
(payload as any).schedule = {
|
||||
kind: originalSchedule.value.kind,
|
||||
expr: formData.value.schedule,
|
||||
display: formData.value.schedule,
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await jobsStore.updateJob(props.jobId!, payload)
|
||||
message.success(t('jobs.jobUpdated'))
|
||||
|
||||
@@ -22,25 +22,6 @@ function isSaving(platform: string, field: string) {
|
||||
return !!saving[savingKey(platform, field)]
|
||||
}
|
||||
|
||||
// Debounce timers
|
||||
const debounceTimers: Record<string, ReturnType<typeof setTimeout>> = {}
|
||||
|
||||
function debounceSave(platform: string, field: string, saveFn: () => Promise<void>, delay = 600) {
|
||||
const key = savingKey(platform, field)
|
||||
if (debounceTimers[key]) clearTimeout(debounceTimers[key])
|
||||
debounceTimers[key] = setTimeout(async () => {
|
||||
saving[key] = true
|
||||
try {
|
||||
await saveFn()
|
||||
message.success(t('settings.saved'))
|
||||
} catch (err: any) {
|
||||
message.error(t('settings.saveFailed'))
|
||||
} finally {
|
||||
saving[key] = false
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
// Immediate save for switches
|
||||
async function immediateSave(platform: string, field: string, saveFn: () => Promise<void>) {
|
||||
const key = savingKey(platform, field)
|
||||
@@ -59,10 +40,6 @@ async function saveChannel(platform: string, field: string, values: Record<strin
|
||||
immediateSave(platform, field, () => settingsStore.saveSection(platform, values))
|
||||
}
|
||||
|
||||
function debouncedSaveChannel(platform: string, field: string, values: Record<string, any>) {
|
||||
debounceSave(platform, field, () => settingsStore.saveSection(platform, values))
|
||||
}
|
||||
|
||||
// Save credentials to .env (matching hermes gateway setup behavior)
|
||||
async function saveCredentials(platform: string, field: string, values: Record<string, any>) {
|
||||
immediateSave(platform, field, async () => {
|
||||
@@ -71,13 +48,6 @@ async function saveCredentials(platform: string, field: string, values: Record<s
|
||||
})
|
||||
}
|
||||
|
||||
function debouncedSaveCredentials(platform: string, field: string, values: Record<string, any>) {
|
||||
debounceSave(platform, field, async () => {
|
||||
await saveCredsApi(platform, values)
|
||||
await settingsStore.fetchSettings()
|
||||
})
|
||||
}
|
||||
|
||||
function getCreds(key: string) {
|
||||
return (settingsStore.platforms[key] || {}) as Record<string, any>
|
||||
}
|
||||
@@ -121,7 +91,6 @@ function pollWeixinStatus() {
|
||||
wxQrStatus.value = 'expired'
|
||||
} else if (data.status === 'confirmed') {
|
||||
wxQrStatus.value = 'confirmed'
|
||||
// Save credentials to .env
|
||||
await saveWeixinCredentials({
|
||||
account_id: data.account_id!,
|
||||
token: data.token!,
|
||||
@@ -131,7 +100,6 @@ function pollWeixinStatus() {
|
||||
message.success(t('settings.saved'))
|
||||
}
|
||||
} catch {
|
||||
// Retry poll on network error
|
||||
pollWeixinStatus()
|
||||
}
|
||||
}, 3000)
|
||||
@@ -146,7 +114,6 @@ function stopWeixinPoll() {
|
||||
|
||||
onUnmounted(() => {
|
||||
stopWeixinPoll()
|
||||
Object.values(debounceTimers).forEach(t => clearTimeout(t))
|
||||
})
|
||||
|
||||
const platforms = [
|
||||
@@ -180,11 +147,6 @@ const platforms = [
|
||||
name: 'Feishu',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.59 3.41a2.25 2.25 0 0 1 3.182 0L13.5 7.14l-3.182 3.182L6.59 7.59a2.25 2.25 0 0 1 0-3.182zm5.303 5.303L15.075 5.53a2.25 2.25 0 0 1 3.182 3.182L15.075 11.894 11.893 8.713zM3.41 6.59a2.25 2.25 0 0 1 3.182 0l3.182 3.182-3.182 3.182a2.25 2.25 0 0 1-3.182-3.182L3.41 6.59zm5.303 5.303L11.894 15.075a2.25 2.25 0 0 1-3.182 3.182L5.53 15.075 8.713 11.893zm5.303-5.303L17.478 9.778a2.25 2.25 0 0 1-3.182 3.182L10.53 10.075l3.182-3.182 0 .023z"/></svg>',
|
||||
},
|
||||
// {
|
||||
// key: 'dingtalk',
|
||||
// name: 'DingTalk',
|
||||
// icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221l-1.897 6.376a.5.5 0 0 1-.957-.016l-1.238-3.81a.5.5 0 0 0-.477-.354l-3.81-.324a.5.5 0 0 1-.074-.993l6.376-1.897a.5.5 0 0 1 .577.718z"/></svg>',
|
||||
// },
|
||||
{
|
||||
key: 'weixin',
|
||||
name: 'Weixin',
|
||||
@@ -211,7 +173,7 @@ const platforms = [
|
||||
<!-- Telegram -->
|
||||
<template v-if="p.key === 'telegram'">
|
||||
<SettingRow :label="t('platform.botToken')" :hint="t('platform.botTokenHint')">
|
||||
<NInput :value="getCreds('telegram').token || ''" :loading="isSaving('telegram', 'token')" clearable size="small" class="input-lg" placeholder="123456:ABC-DEF..." @update:value="v => debouncedSaveCredentials('telegram', 'token', { token: v })" />
|
||||
<NInput :default-value="getCreds('telegram').token || ''" :loading="isSaving('telegram', 'token')" clearable size="small" class="input-lg" placeholder="123456:ABC-DEF..." @change="v => saveCredentials('telegram', 'token', { token: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionGroup')">
|
||||
<NSwitch :value="settingsStore.telegram.require_mention" :loading="isSaving('telegram', 'require_mention')" @update:value="v => saveChannel('telegram', 'require_mention', { require_mention: v })" />
|
||||
@@ -220,17 +182,17 @@ const platforms = [
|
||||
<NSwitch :value="settingsStore.telegram.reactions" :loading="isSaving('telegram', 'reactions')" @update:value="v => saveChannel('telegram', 'reactions', { reactions: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChats')" :hint="t('platform.freeResponseChatsHint')">
|
||||
<NInput :value="settingsStore.telegram.free_response_chats || ''" :loading="isSaving('telegram', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @update:value="v => debouncedSaveChannel('telegram', 'free_response_chats', { free_response_chats: v })" />
|
||||
<NInput :default-value="settingsStore.telegram.free_response_chats || ''" :loading="isSaving('telegram', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @change="v => saveChannel('telegram', 'free_response_chats', { free_response_chats: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.mentionPatterns')" :hint="t('platform.mentionPatternsHint')">
|
||||
<NInput :value="(settingsStore.telegram.mention_patterns || []).join(', ')" :loading="isSaving('telegram', 'mention_patterns')" size="small" placeholder="pattern1, pattern2" @update:value="v => debouncedSaveChannel('telegram', 'mention_patterns', { mention_patterns: v ? v.split(',').map(s => s.trim()) : [] })" />
|
||||
<NInput :default-value="(settingsStore.telegram.mention_patterns || []).join(', ')" :loading="isSaving('telegram', 'mention_patterns')" size="small" placeholder="pattern1, pattern2" @change="v => saveChannel('telegram', 'mention_patterns', { mention_patterns: v ? v.split(',').map(s => s.trim()) : [] })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- Discord -->
|
||||
<template v-if="p.key === 'discord'">
|
||||
<SettingRow :label="t('platform.botToken')" :hint="t('platform.botTokenHint')">
|
||||
<NInput :value="getCreds('discord').token || ''" :loading="isSaving('discord', 'token')" clearable size="small" class="input-lg" placeholder="Bot token..." @update:value="v => debouncedSaveCredentials('discord', 'token', { token: v })" />
|
||||
<NInput :default-value="getCreds('discord').token || ''" :loading="isSaving('discord', 'token')" clearable size="small" class="input-lg" placeholder="Bot token..." @change="v => saveCredentials('discord', 'token', { token: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionChannel')">
|
||||
<NSwitch :value="settingsStore.discord.require_mention" :loading="isSaving('discord', 'require_mention')" @update:value="v => saveChannel('discord', 'require_mention', { require_mention: v })" />
|
||||
@@ -242,23 +204,23 @@ const platforms = [
|
||||
<NSwitch :value="settingsStore.discord.reactions" :loading="isSaving('discord', 'reactions')" @update:value="v => saveChannel('discord', 'reactions', { reactions: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChannels')" :hint="t('platform.freeResponseChannelsHint')">
|
||||
<NInput :value="settingsStore.discord.free_response_channels || ''" :loading="isSaving('discord', 'free_response_channels')" size="small" placeholder="channel_id1,channel_id2" @update:value="v => debouncedSaveChannel('discord', 'free_response_channels', { free_response_channels: v })" />
|
||||
<NInput :default-value="settingsStore.discord.free_response_channels || ''" :loading="isSaving('discord', 'free_response_channels')" size="small" placeholder="channel_id1,channel_id2" @change="v => saveChannel('discord', 'free_response_channels', { free_response_channels: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.allowedChannels')" :hint="t('platform.allowedChannelsHint')">
|
||||
<NInput :value="settingsStore.discord.allowed_channels || ''" :loading="isSaving('discord', 'allowed_channels')" size="small" placeholder="channel_id1,channel_id2" @update:value="v => debouncedSaveChannel('discord', 'allowed_channels', { allowed_channels: v })" />
|
||||
<NInput :default-value="settingsStore.discord.allowed_channels || ''" :loading="isSaving('discord', 'allowed_channels')" size="small" placeholder="channel_id1,channel_id2" @change="v => saveChannel('discord', 'allowed_channels', { allowed_channels: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.ignoredChannels')" :hint="t('platform.ignoredChannelsHint')">
|
||||
<NInput :value="settingsStore.discord.ignored_channels || ''" :loading="isSaving('discord', 'ignored_channels')" size="small" placeholder="channel_id1,channel_id2" @update:value="v => debouncedSaveChannel('discord', 'ignored_channels', { ignored_channels: v })" />
|
||||
<NInput :default-value="settingsStore.discord.ignored_channels || ''" :loading="isSaving('discord', 'ignored_channels')" size="small" placeholder="channel_id1,channel_id2" @change="v => saveChannel('discord', 'ignored_channels', { ignored_channels: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.noThreadChannels')" :hint="t('platform.noThreadChannelsHint')">
|
||||
<NInput :value="settingsStore.discord.no_thread_channels || ''" :loading="isSaving('discord', 'no_thread_channels')" size="small" placeholder="channel_id1,channel_id2" @update:value="v => debouncedSaveChannel('discord', 'no_thread_channels', { no_thread_channels: v })" />
|
||||
<NInput :default-value="settingsStore.discord.no_thread_channels || ''" :loading="isSaving('discord', 'no_thread_channels')" size="small" placeholder="channel_id1,channel_id2" @change="v => saveChannel('discord', 'no_thread_channels', { no_thread_channels: v })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- Slack -->
|
||||
<template v-if="p.key === 'slack'">
|
||||
<SettingRow :label="t('platform.botToken')" :hint="t('platform.botTokenHint')">
|
||||
<NInput :value="getCreds('slack').token || ''" :loading="isSaving('slack', 'token')" clearable size="small" class="input-lg" placeholder="xoxb-..." @update:value="v => debouncedSaveCredentials('slack', 'token', { token: v })" />
|
||||
<NInput :default-value="getCreds('slack').token || ''" :loading="isSaving('slack', 'token')" clearable size="small" class="input-lg" placeholder="xoxb-..." @change="v => saveCredentials('slack', 'token', { token: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionChannel')">
|
||||
<NSwitch :value="settingsStore.slack.require_mention" :loading="isSaving('slack', 'require_mention')" @update:value="v => saveChannel('slack', 'require_mention', { require_mention: v })" />
|
||||
@@ -267,7 +229,7 @@ const platforms = [
|
||||
<NSwitch :value="settingsStore.slack.allow_bots" :loading="isSaving('slack', 'allow_bots')" @update:value="v => saveChannel('slack', 'allow_bots', { allow_bots: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChannels')" :hint="t('platform.freeResponseChannelsHint')">
|
||||
<NInput :value="settingsStore.slack.free_response_channels || ''" :loading="isSaving('slack', 'free_response_channels')" size="small" placeholder="channel_id1,channel_id2" @update:value="v => debouncedSaveChannel('slack', 'free_response_channels', { free_response_channels: v })" />
|
||||
<NInput :default-value="settingsStore.slack.free_response_channels || ''" :loading="isSaving('slack', 'free_response_channels')" size="small" placeholder="channel_id1,channel_id2" @change="v => saveChannel('slack', 'free_response_channels', { free_response_channels: v })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
@@ -280,20 +242,20 @@ const platforms = [
|
||||
<NSwitch :value="settingsStore.whatsapp.require_mention" :loading="isSaving('whatsapp', 'require_mention')" @update:value="v => saveChannel('whatsapp', 'require_mention', { require_mention: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChats')" :hint="t('platform.freeResponseChatsHint')">
|
||||
<NInput :value="settingsStore.whatsapp.free_response_chats || ''" :loading="isSaving('whatsapp', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @update:value="v => debouncedSaveChannel('whatsapp', 'free_response_chats', { free_response_chats: v })" />
|
||||
<NInput :default-value="settingsStore.whatsapp.free_response_chats || ''" :loading="isSaving('whatsapp', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @change="v => saveChannel('whatsapp', 'free_response_chats', { free_response_chats: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.mentionPatterns')" :hint="t('platform.mentionPatternsHint')">
|
||||
<NInput :value="(settingsStore.whatsapp.mention_patterns || []).join(', ')" :loading="isSaving('whatsapp', 'mention_patterns')" size="small" placeholder="pattern1, pattern2" @update:value="v => debouncedSaveChannel('whatsapp', 'mention_patterns', { mention_patterns: v ? v.split(',').map(s => s.trim()) : [] })" />
|
||||
<NInput :default-value="(settingsStore.whatsapp.mention_patterns || []).join(', ')" :loading="isSaving('whatsapp', 'mention_patterns')" size="small" placeholder="pattern1, pattern2" @change="v => saveChannel('whatsapp', 'mention_patterns', { mention_patterns: v ? v.split(',').map(s => s.trim()) : [] })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- Matrix -->
|
||||
<template v-if="p.key === 'matrix'">
|
||||
<SettingRow :label="t('platform.accessToken')" :hint="t('platform.accessTokenHint')">
|
||||
<NInput :value="getCreds('matrix').token || ''" :loading="isSaving('matrix', 'token')" clearable size="small" class="input-lg" placeholder="syt_..." @update:value="v => debouncedSaveCredentials('matrix', 'token', { token: v })" />
|
||||
<NInput :default-value="getCreds('matrix').token || ''" :loading="isSaving('matrix', 'token')" clearable size="small" class="input-lg" placeholder="syt_..." @change="v => saveCredentials('matrix', 'token', { token: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.homeserver')" :hint="t('platform.homeserverHint')">
|
||||
<NInput :value="getCreds('matrix').extra?.homeserver || ''" :loading="isSaving('matrix', 'homeserver')" clearable size="small" class="input-lg" placeholder="https://matrix.org" @update:value="v => debouncedSaveCredentials('matrix', 'homeserver', { extra: { ...getCreds('matrix').extra, homeserver: v } })" />
|
||||
<NInput :default-value="getCreds('matrix').extra?.homeserver || ''" :loading="isSaving('matrix', 'homeserver')" clearable size="small" class="input-lg" placeholder="https://matrix.org" @change="v => saveCredentials('matrix', 'homeserver', { extra: { ...getCreds('matrix').extra, homeserver: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionRoom')">
|
||||
<NSwitch :value="settingsStore.matrix.require_mention" :loading="isSaving('matrix', 'require_mention')" @update:value="v => saveChannel('matrix', 'require_mention', { require_mention: v })" />
|
||||
@@ -305,39 +267,39 @@ const platforms = [
|
||||
<NSwitch :value="settingsStore.matrix.dm_mention_threads" :loading="isSaving('matrix', 'dm_mention_threads')" @update:value="v => saveChannel('matrix', 'dm_mention_threads', { dm_mention_threads: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseRooms')" :hint="t('platform.freeResponseRoomsHint')">
|
||||
<NInput :value="settingsStore.matrix.free_response_rooms || ''" :loading="isSaving('matrix', 'free_response_rooms')" size="small" placeholder="room_id1,room_id2" @update:value="v => debouncedSaveChannel('matrix', 'free_response_rooms', { free_response_rooms: v })" />
|
||||
<NInput :default-value="settingsStore.matrix.free_response_rooms || ''" :loading="isSaving('matrix', 'free_response_rooms')" size="small" placeholder="room_id1,room_id2" @change="v => saveChannel('matrix', 'free_response_rooms', { free_response_rooms: v })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- Feishu -->
|
||||
<template v-if="p.key === 'feishu'">
|
||||
<SettingRow :label="t('platform.appId')" :hint="t('platform.appIdHint')">
|
||||
<NInput :value="getCreds('feishu').extra?.app_id || ''" :loading="isSaving('feishu', 'app_id')" clearable size="small" class="input-lg" placeholder="cli_..." @update:value="v => debouncedSaveCredentials('feishu', 'app_id', { extra: { ...getCreds('feishu').extra, app_id: v } })" />
|
||||
<NInput :default-value="getCreds('feishu').extra?.app_id || ''" :loading="isSaving('feishu', 'app_id')" clearable size="small" class="input-lg" placeholder="cli_..." @change="v => saveCredentials('feishu', 'app_id', { extra: { ...getCreds('feishu').extra, app_id: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.appSecret')" :hint="t('platform.appSecretHint')">
|
||||
<NInput :value="getCreds('feishu').extra?.app_secret || ''" :loading="isSaving('feishu', 'app_secret')" clearable size="small" class="input-lg" placeholder="App Secret" @update:value="v => debouncedSaveCredentials('feishu', 'app_secret', { extra: { ...getCreds('feishu').extra, app_secret: v } })" />
|
||||
<NInput :default-value="getCreds('feishu').extra?.app_secret || ''" :loading="isSaving('feishu', 'app_secret')" clearable size="small" class="input-lg" placeholder="App Secret" @change="v => saveCredentials('feishu', 'app_secret', { extra: { ...getCreds('feishu').extra, app_secret: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionGroup')">
|
||||
<NSwitch :value="settingsStore.feishu.require_mention" :loading="isSaving('feishu', 'require_mention')" @update:value="v => saveChannel('feishu', 'require_mention', { require_mention: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChats')" :hint="t('platform.freeResponseChatsHint')">
|
||||
<NInput :value="settingsStore.feishu.free_response_chats || ''" :loading="isSaving('feishu', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @update:value="v => debouncedSaveChannel('feishu', 'free_response_chats', { free_response_chats: v })" />
|
||||
<NInput :default-value="settingsStore.feishu.free_response_chats || ''" :loading="isSaving('feishu', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @change="v => saveChannel('feishu', 'free_response_chats', { free_response_chats: v })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- DingTalk -->
|
||||
<template v-if="p.key === 'dingtalk'">
|
||||
<SettingRow :label="t('platform.clientId')" :hint="t('platform.clientIdHint')">
|
||||
<NInput :value="getCreds('dingtalk').extra?.client_id || ''" :loading="isSaving('dingtalk', 'client_id')" clearable size="small" class="input-lg" placeholder="Client ID" @update:value="v => debouncedSaveCredentials('dingtalk', 'client_id', { extra: { ...getCreds('dingtalk').extra, client_id: v } })" />
|
||||
<NInput :default-value="getCreds('dingtalk').extra?.client_id || ''" :loading="isSaving('dingtalk', 'client_id')" clearable size="small" class="input-lg" placeholder="Client ID" @change="v => saveCredentials('dingtalk', 'client_id', { extra: { ...getCreds('dingtalk').extra, client_id: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.clientSecret')" :hint="t('platform.clientSecretHint')">
|
||||
<NInput :value="getCreds('dingtalk').extra?.client_secret || ''" :loading="isSaving('dingtalk', 'client_secret')" clearable size="small" class="input-lg" placeholder="Client Secret" @update:value="v => debouncedSaveCredentials('dingtalk', 'client_secret', { extra: { ...getCreds('dingtalk').extra, client_secret: v } })" />
|
||||
<NInput :default-value="getCreds('dingtalk').extra?.client_secret || ''" :loading="isSaving('dingtalk', 'client_secret')" clearable size="small" class="input-lg" placeholder="Client Secret" @change="v => saveCredentials('dingtalk', 'client_secret', { extra: { ...getCreds('dingtalk').extra, client_secret: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.requireMention')" :hint="t('platform.requireMentionGroup')">
|
||||
<NSwitch :value="settingsStore.dingtalk.require_mention" :loading="isSaving('dingtalk', 'require_mention')" @update:value="v => saveChannel('dingtalk', 'require_mention', { require_mention: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.freeResponseChats')" :hint="t('platform.freeResponseChatsHint')">
|
||||
<NInput :value="settingsStore.dingtalk.free_response_chats || ''" :loading="isSaving('dingtalk', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @update:value="v => debouncedSaveChannel('dingtalk', 'free_response_chats', { free_response_chats: v })" />
|
||||
<NInput :default-value="settingsStore.dingtalk.free_response_chats || ''" :loading="isSaving('dingtalk', 'free_response_chats')" size="small" placeholder="chat_id1,chat_id2" @change="v => saveChannel('dingtalk', 'free_response_chats', { free_response_chats: v })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
@@ -361,20 +323,20 @@ const platforms = [
|
||||
</div>
|
||||
</div>
|
||||
<SettingRow :label="t('platform.weixinToken')" :hint="t('platform.weixinTokenHint')">
|
||||
<NInput :value="getCreds('weixin').token || ''" :loading="isSaving('weixin', 'token')" clearable size="small" class="input-lg" placeholder="Token" @update:value="v => debouncedSaveCredentials('weixin', 'token', { token: v })" />
|
||||
<NInput :default-value="getCreds('weixin').token || ''" :loading="isSaving('weixin', 'token')" clearable size="small" class="input-lg" placeholder="Token" @change="v => saveCredentials('weixin', 'token', { token: v })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.accountId')" :hint="t('platform.accountIdHint')">
|
||||
<NInput :value="getCreds('weixin').extra?.account_id || ''" :loading="isSaving('weixin', 'account_id')" clearable size="small" class="input-lg" placeholder="Account ID" @update:value="v => debouncedSaveCredentials('weixin', 'account_id', { extra: { ...getCreds('weixin').extra, account_id: v } })" />
|
||||
<NInput :default-value="getCreds('weixin').extra?.account_id || ''" :loading="isSaving('weixin', 'account_id')" clearable size="small" class="input-lg" placeholder="Account ID" @change="v => saveCredentials('weixin', 'account_id', { extra: { ...getCreds('weixin').extra, account_id: v } })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
|
||||
<!-- WeCom -->
|
||||
<template v-if="p.key === 'wecom'">
|
||||
<SettingRow :label="t('platform.botId')" :hint="t('platform.botIdHint')">
|
||||
<NInput :value="getCreds('wecom').extra?.bot_id || ''" :loading="isSaving('wecom', 'bot_id')" clearable size="small" class="input-lg" placeholder="Bot ID" @update:value="v => debouncedSaveCredentials('wecom', 'bot_id', { extra: { ...getCreds('wecom').extra, bot_id: v } })" />
|
||||
<NInput :default-value="getCreds('wecom').extra?.bot_id || ''" :loading="isSaving('wecom', 'bot_id')" clearable size="small" class="input-lg" placeholder="Bot ID" @change="v => saveCredentials('wecom', 'bot_id', { extra: { ...getCreds('wecom').extra, bot_id: v } })" />
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('platform.appSecret')" :hint="t('platform.wecomSecretHint')">
|
||||
<NInput :value="getCreds('wecom').extra?.secret || ''" :loading="isSaving('wecom', 'secret')" clearable size="small" class="input-lg" placeholder="Secret" @update:value="v => debouncedSaveCredentials('wecom', 'secret', { extra: { ...getCreds('wecom').extra, secret: v } })" />
|
||||
<NInput :default-value="getCreds('wecom').extra?.secret || ''" :loading="isSaving('wecom', 'secret')" clearable size="small" class="input-lg" placeholder="Secret" @change="v => saveCredentials('wecom', 'secret', { extra: { ...getCreds('wecom').extra, secret: v } })" />
|
||||
</SettingRow>
|
||||
</template>
|
||||
</PlatformCard>
|
||||
|
||||
@@ -86,13 +86,13 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
label: 'MiniMax',
|
||||
value: 'minimax',
|
||||
base_url: 'https://api.minimax.io/anthropic/v1',
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2'],
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed'],
|
||||
},
|
||||
{
|
||||
label: 'MiniMax (China)',
|
||||
value: 'minimax-cn',
|
||||
base_url: 'https://api.minimaxi.com/v1',
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2'],
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed'],
|
||||
},
|
||||
{
|
||||
label: 'Alibaba Cloud',
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { NTabs, NTabPane, NSpin, NSwitch, NInput, NInputNumber, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSettingsStore } from '@/stores/hermes/settings'
|
||||
import DisplaySettings from '@/components/hermes/settings/DisplaySettings.vue'
|
||||
import AgentSettings from '@/components/hermes/settings/AgentSettings.vue'
|
||||
import MemorySettings from '@/components/hermes/settings/MemorySettings.vue'
|
||||
import SessionSettings from '@/components/hermes/settings/SessionSettings.vue'
|
||||
import PrivacySettings from '@/components/hermes/settings/PrivacySettings.vue'
|
||||
import SettingRow from '@/components/hermes/settings/SettingRow.vue'
|
||||
import { onMounted } from "vue";
|
||||
import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NSpin,
|
||||
NSwitch,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
useMessage,
|
||||
} from "naive-ui";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSettingsStore } from "@/stores/hermes/settings";
|
||||
import DisplaySettings from "@/components/hermes/settings/DisplaySettings.vue";
|
||||
import AgentSettings from "@/components/hermes/settings/AgentSettings.vue";
|
||||
import MemorySettings from "@/components/hermes/settings/MemorySettings.vue";
|
||||
import SessionSettings from "@/components/hermes/settings/SessionSettings.vue";
|
||||
import PrivacySettings from "@/components/hermes/settings/PrivacySettings.vue";
|
||||
import SettingRow from "@/components/hermes/settings/SettingRow.vue";
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
const settingsStore = useSettingsStore();
|
||||
const message = useMessage();
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
settingsStore.fetchSettings()
|
||||
})
|
||||
|
||||
settingsStore.fetchSettings();
|
||||
});
|
||||
async function saveApiServer(values: Record<string, any>) {
|
||||
try {
|
||||
await settingsStore.saveSection('platforms', { api_server: values })
|
||||
message.success(t('settings.saved'))
|
||||
await settingsStore.saveSection("platforms", { api_server: values });
|
||||
message.success(t("settings.saved"));
|
||||
} catch (err: any) {
|
||||
message.error(t('settings.saveFailed'))
|
||||
message.error(t("settings.saveFailed"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -31,11 +38,15 @@ async function saveApiServer(values: Record<string, any>) {
|
||||
<template>
|
||||
<div class="settings-view">
|
||||
<header class="page-header">
|
||||
<h2 class="header-title">{{ t('settings.title') }}</h2>
|
||||
<h2 class="header-title">{{ t("settings.title") }}</h2>
|
||||
</header>
|
||||
|
||||
<div class="settings-content">
|
||||
<NSpin :show="settingsStore.loading || settingsStore.saving" size="large" :description="t('common.loading')">
|
||||
<NSpin
|
||||
:show="settingsStore.loading || settingsStore.saving"
|
||||
size="large"
|
||||
:description="t('common.loading')"
|
||||
>
|
||||
<NTabs type="line" animated>
|
||||
<NTabPane name="display" :tab="t('settings.tabs.display')">
|
||||
<DisplaySettings />
|
||||
@@ -54,40 +65,66 @@ async function saveApiServer(values: Record<string, any>) {
|
||||
</NTabPane>
|
||||
<NTabPane name="api_server" :tab="t('settings.tabs.apiServer')">
|
||||
<section class="settings-section">
|
||||
<SettingRow :label="t('settings.apiServer.enable')" :hint="t('settings.apiServer.enableHint')">
|
||||
<SettingRow
|
||||
:label="t('settings.apiServer.enable')"
|
||||
:hint="t('settings.apiServer.enableHint')"
|
||||
>
|
||||
<NSwitch
|
||||
:value="settingsStore.platforms?.api_server?.enabled"
|
||||
@update:value="v => saveApiServer({ enabled: v })"
|
||||
@update:value="(v) => saveApiServer({ enabled: v })"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('settings.apiServer.host')" :hint="t('settings.apiServer.hostHint')">
|
||||
<SettingRow
|
||||
:label="t('settings.apiServer.host')"
|
||||
:hint="t('settings.apiServer.hostHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="settingsStore.platforms?.api_server?.host || ''"
|
||||
size="small" class="input-md"
|
||||
@update:value="v => saveApiServer({ host: v })"
|
||||
:default-value="settingsStore.platforms?.api_server?.host || ''"
|
||||
size="small"
|
||||
class="input-md"
|
||||
@change="(v: string) => saveApiServer({ host: v })"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('settings.apiServer.port')" :hint="t('settings.apiServer.portHint')">
|
||||
<SettingRow
|
||||
:label="t('settings.apiServer.port')"
|
||||
:hint="t('settings.apiServer.portHint')"
|
||||
>
|
||||
<NInputNumber
|
||||
:value="settingsStore.platforms?.api_server?.port"
|
||||
:min="1024" :max="65535"
|
||||
size="small" class="input-sm"
|
||||
@update:value="v => v != null && saveApiServer({ port: v })"
|
||||
:default-value="settingsStore.platforms?.api_server?.port"
|
||||
:min="1024"
|
||||
:max="65535"
|
||||
size="small"
|
||||
class="input-sm"
|
||||
@blur="(e: FocusEvent) => {
|
||||
const val = (e.target as HTMLInputElement).value
|
||||
if (val) saveApiServer({ port: Number(val) })
|
||||
}"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('settings.apiServer.key')" :hint="t('settings.apiServer.keyHint')">
|
||||
<SettingRow
|
||||
:label="t('settings.apiServer.key')"
|
||||
:hint="t('settings.apiServer.keyHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="settingsStore.platforms?.api_server?.key || ''"
|
||||
type="password" show-password-on="click"
|
||||
size="small" class="input-md"
|
||||
@update:value="v => saveApiServer({ key: v })"
|
||||
:default-value="settingsStore.platforms?.api_server?.key || ''"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
size="small"
|
||||
class="input-md"
|
||||
@change="(v: string) => saveApiServer({ key: v })"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow :label="t('settings.apiServer.cors')" :hint="t('settings.apiServer.corsHint')">
|
||||
<SettingRow
|
||||
:label="t('settings.apiServer.cors')"
|
||||
:hint="t('settings.apiServer.corsHint')"
|
||||
>
|
||||
<NInput
|
||||
:value="settingsStore.platforms?.api_server?.cors_origins || ''"
|
||||
size="small" class="input-md"
|
||||
@update:value="v => saveApiServer({ cors_origins: v })"
|
||||
:default-value="
|
||||
settingsStore.platforms?.api_server?.cors_origins || ''
|
||||
"
|
||||
size="small"
|
||||
class="input-md"
|
||||
@change="(v: string) => saveApiServer({ cors_origins: v })"
|
||||
/>
|
||||
</SettingRow>
|
||||
</section>
|
||||
@@ -99,7 +136,7 @@ async function saveApiServer(values: Record<string, any>) {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/variables' as *;
|
||||
@use "@/styles/variables" as *;
|
||||
|
||||
.settings-view {
|
||||
height: calc(100 * var(--vh));
|
||||
|
||||
@@ -10,7 +10,7 @@ import { config } from './config'
|
||||
import { hermesRoutes, setupTerminalWebSocket, proxyMiddleware } from './routes/hermes'
|
||||
import { uploadRoutes } from './routes/upload'
|
||||
import { webhookRoutes } from './routes/webhook'
|
||||
import * as hermesCli from './services/hermes-cli'
|
||||
import * as hermesCli from './services/hermes/hermes-cli'
|
||||
import { getToken, authMiddleware } from './services/auth'
|
||||
|
||||
function getLocalVersion(): string {
|
||||
@@ -232,7 +232,7 @@ function bindShutdown() {
|
||||
async function ensureApiServerConfig() {
|
||||
const { readFileSync, writeFileSync, existsSync, copyFileSync } = await import('fs')
|
||||
const yaml = (await import('js-yaml')).default
|
||||
const { getActiveConfigPath } = await import('./services/hermes-profile')
|
||||
const { getActiveConfigPath } = await import('./services/hermes/hermes-profile')
|
||||
const configPath = getActiveConfigPath()
|
||||
|
||||
const defaults: Record<string, any> = {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { readFile, writeFile, copyFile } from 'fs/promises'
|
||||
import { chmod } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import YAML from 'js-yaml'
|
||||
import { restartGateway } from '../../services/hermes-cli'
|
||||
import { getActiveConfigPath, getActiveEnvPath, getActiveProfileDir } from '../../services/hermes-profile'
|
||||
import { restartGateway } from '../../services/hermes/hermes-cli'
|
||||
import { getActiveConfigPath, getActiveEnvPath, getActiveProfileDir } from '../../services/hermes/hermes-profile'
|
||||
|
||||
// Platform sections that require gateway restart after config change
|
||||
const PLATFORM_SECTIONS = new Set([
|
||||
@@ -79,6 +79,20 @@ function getNested(obj: Record<string, any>, path: string): any {
|
||||
return cur
|
||||
}
|
||||
|
||||
function deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (
|
||||
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
|
||||
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
|
||||
) {
|
||||
target[key] = deepMerge(target[key], source[key])
|
||||
} else {
|
||||
target[key] = source[key]
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
async function readEnvPlatforms(): Promise<Record<string, any>> {
|
||||
try {
|
||||
const raw = await readFile(envPath(), 'utf-8')
|
||||
@@ -217,7 +231,7 @@ configRoutes.put('/api/hermes/config', async (ctx) => {
|
||||
|
||||
try {
|
||||
const config = await readConfig()
|
||||
config[section] = { ...(config[section] || {}), ...values }
|
||||
config[section] = deepMerge(config[section] || {}, values)
|
||||
await writeConfig(config)
|
||||
// Restart gateway for platform/channel config changes
|
||||
if (PLATFORM_SECTIONS.has(section)) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import Router from '@koa/router'
|
||||
import { readdir, readFile, stat, writeFile, mkdir, copyFile } from 'fs/promises'
|
||||
import { join, resolve } from 'path'
|
||||
import YAML from 'js-yaml'
|
||||
import { getActiveProfileDir, getActiveConfigPath, getActiveAuthPath, getActiveEnvPath } from '../../services/hermes-profile'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import { getActiveProfileDir, getActiveConfigPath, getActiveAuthPath, getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
// --- Provider env var mapping (from hermes providers.py HERMES_OVERLAYS + config.py) ---
|
||||
// Maps provider key → { api_key_envs: all env var aliases for API key, base_url_env: env var for base URL }
|
||||
@@ -513,21 +513,40 @@ fsRoutes.get('/api/hermes/available-models', async (ctx) => {
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value.models.length > 0) {
|
||||
const { key, label, base_url, models } = result.value
|
||||
groups.push({ provider: key, label, base_url, models })
|
||||
groups.push({ provider: key, label, base_url, models: Array.from(new Set(models)) })
|
||||
} else if (result.status === 'rejected') {
|
||||
console.error(`[available-models] Failed: ${result.reason?.message || result.reason}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate models within each group and merge groups with the same provider key
|
||||
const dedupedGroups: typeof groups = []
|
||||
const seenProviders = new Map<string, number>()
|
||||
for (const g of groups) {
|
||||
g.models = Array.from(new Set(g.models))
|
||||
const existingIdx = seenProviders.get(g.provider)
|
||||
if (existingIdx !== undefined) {
|
||||
// Merge models into existing group
|
||||
const existing = dedupedGroups[existingIdx]
|
||||
const existingSet = new Set(existing.models)
|
||||
for (const m of g.models) {
|
||||
if (!existingSet.has(m)) existing.models.push(m)
|
||||
}
|
||||
} else {
|
||||
seenProviders.set(g.provider, dedupedGroups.length)
|
||||
dedupedGroups.push(g)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no providers returned models, fall back to config.yaml parsing
|
||||
if (groups.length === 0) {
|
||||
if (dedupedGroups.length === 0) {
|
||||
const fallback = buildModelGroups(config)
|
||||
ctx.body = fallback
|
||||
return
|
||||
}
|
||||
|
||||
ctx.body = { default: currentDefault, groups }
|
||||
ctx.body = { default: currentDefault, groups: dedupedGroups }
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { error: err.message }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
export const logRoutes = new Router()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdir, writeFile } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { tmpdir, homedir } from 'os'
|
||||
import YAML from 'js-yaml'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
const apiServerDefaults = {
|
||||
enabled: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import * as hermesCli from '../../services/hermes-cli'
|
||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||
|
||||
export const sessionRoutes = new Router()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import axios from 'axios'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
import { chmod } from 'fs/promises'
|
||||
import { resolve } from 'path'
|
||||
import { restartGateway } from '../../services/hermes-cli'
|
||||
import { getActiveEnvPath } from '../../services/hermes-profile'
|
||||
import { restartGateway } from '../../services/hermes/hermes-cli'
|
||||
import { getActiveEnvPath } from '../../services/hermes/hermes-profile'
|
||||
|
||||
const envPath = () => getActiveEnvPath()
|
||||
const ILINK_BASE = 'https://ilinkai.weixin.qq.com'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from '@koa/router'
|
||||
import { emitWebhook } from '../services/hermes'
|
||||
import { emitWebhook } from '../services/hermes/hermes'
|
||||
|
||||
export const webhookRoutes = new Router()
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { config } from '../config'
|
||||
import { config } from '../../config'
|
||||
|
||||
const UPSTREAM = config.upstream.replace(/\/$/, '')
|
||||
|
||||
@@ -86,13 +86,13 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
label: 'MiniMax',
|
||||
value: 'minimax',
|
||||
base_url: 'https://api.minimax.io/anthropic/v1',
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2'],
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed'],
|
||||
},
|
||||
{
|
||||
label: 'MiniMax (China)',
|
||||
value: 'minimax-cn',
|
||||
base_url: 'https://api.minimaxi.com/v1',
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2'],
|
||||
models: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed'],
|
||||
},
|
||||
{
|
||||
label: 'Alibaba Cloud',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock hermes-cli
|
||||
vi.mock('../../packages/server/src/services/hermes-cli', () => ({
|
||||
vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({
|
||||
listProfiles: vi.fn(),
|
||||
getProfile: vi.fn(),
|
||||
createProfile: vi.fn(),
|
||||
@@ -16,7 +16,7 @@ vi.mock('../../packages/server/src/services/hermes-cli', () => ({
|
||||
importProfile: vi.fn(),
|
||||
}))
|
||||
|
||||
import * as hermesCli from '../../packages/server/src/services/hermes-cli'
|
||||
import * as hermesCli from '../../packages/server/src/services/hermes/hermes-cli'
|
||||
|
||||
describe('Profile Routes', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -120,7 +120,15 @@ describe('Proxy Handler', () => {
|
||||
})
|
||||
|
||||
it('returns 502 on connection failure', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'))
|
||||
// waitForGatewayReady loops calling fetch(healthUrl) until res.ok or timeout.
|
||||
// Return ok:true for health checks so the loop exits immediately (gateway
|
||||
// "ready"), then the retry fetch also fails with ECONNREFUSED → 502.
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
if (typeof url === 'string' && url.includes('/health')) {
|
||||
return Promise.resolve({ ok: true })
|
||||
}
|
||||
return Promise.reject(new Error('ECONNREFUSED'))
|
||||
})
|
||||
|
||||
const ctx = createMockCtx()
|
||||
await proxy(ctx)
|
||||
|
||||
Reference in New Issue
Block a user