feat(channel-monitor): 增加监控协议选择界面
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
b447ba6a0d
commit
89d4b0db54
@ -106,9 +106,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { BodyOverrideMode } from '@/api/admin/channelMonitor'
|
||||
import type { APIMode, BodyOverrideMode, Provider } from '@/api/admin/channelMonitor'
|
||||
import {
|
||||
API_MODE_RESPONSES,
|
||||
PROVIDER_OPENAI,
|
||||
} from '@/constants/channelMonitor'
|
||||
|
||||
const props = defineProps<{
|
||||
provider?: Provider
|
||||
apiMode?: APIMode
|
||||
extraHeaders: Record<string, string>
|
||||
bodyOverrideMode: BodyOverrideMode
|
||||
bodyOverride: Record<string, unknown> | null
|
||||
@ -293,6 +299,18 @@ const bodyModeHint = computed(() => {
|
||||
})
|
||||
|
||||
const bodyPlaceholder = computed(() => {
|
||||
if (props.provider === PROVIDER_OPENAI && props.apiMode === API_MODE_RESPONSES) {
|
||||
if (props.bodyOverrideMode === 'merge') {
|
||||
return '{\n "max_output_tokens": 20\n}'
|
||||
}
|
||||
return '{\n "model": "gpt-4o-mini",\n "instructions": "You are a health check endpoint. Reply briefly.",\n "input": "Reply with exactly: ok",\n "max_output_tokens": 20,\n "stream": false\n}'
|
||||
}
|
||||
if (props.provider === PROVIDER_OPENAI) {
|
||||
if (props.bodyOverrideMode === 'merge') {
|
||||
return '{\n "max_tokens": 20\n}'
|
||||
}
|
||||
return '{\n "model": "gpt-4o-mini",\n "messages": [{"role":"user","content":"Reply with exactly: ok"}],\n "max_tokens": 20,\n "stream": false\n}'
|
||||
}
|
||||
if (props.bodyOverrideMode === 'merge') {
|
||||
return '{\n "system": "You are Claude Code..."\n}'
|
||||
}
|
||||
|
||||
@ -29,6 +29,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.provider === PROVIDER_OPENAI" class="rounded-lg border border-blue-100 bg-blue-50/50 p-3 dark:border-blue-500/20 dark:bg-blue-500/10">
|
||||
<label class="input-label">{{ t('admin.channelMonitor.form.apiMode') }}</label>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
v-for="opt in apiModeOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
:aria-pressed="form.api_mode === opt.value"
|
||||
class="rounded-lg border-2 px-3 py-2 text-left transition-colors"
|
||||
:class="apiModeButtonClass(opt.value)"
|
||||
@click="form.api_mode = opt.value"
|
||||
>
|
||||
<span class="block text-sm font-semibold">{{ opt.label }}</span>
|
||||
<span class="mt-0.5 block text-xs opacity-80">{{ opt.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.channelMonitor.form.endpoint') }} <span class="text-red-500">*</span></label>
|
||||
<div class="flex gap-2">
|
||||
@ -115,6 +133,8 @@
|
||||
</div>
|
||||
|
||||
<MonitorAdvancedRequestConfig
|
||||
:provider="form.provider"
|
||||
:api-mode="form.api_mode"
|
||||
:extra-headers="form.extra_headers"
|
||||
:body-override-mode="form.body_override_mode"
|
||||
:body-override="form.body_override"
|
||||
@ -168,6 +188,7 @@ import type {
|
||||
BodyOverrideMode,
|
||||
ChannelMonitor,
|
||||
CreateParams,
|
||||
APIMode,
|
||||
Provider,
|
||||
UpdateParams,
|
||||
} from '@/api/admin/channelMonitor'
|
||||
@ -186,6 +207,8 @@ import {
|
||||
PROVIDER_OPENAI,
|
||||
PROVIDER_ANTHROPIC,
|
||||
PROVIDER_GEMINI,
|
||||
API_MODE_CHAT_COMPLETIONS,
|
||||
API_MODE_RESPONSES,
|
||||
DEFAULT_INTERVAL_SECONDS,
|
||||
} from '@/constants/channelMonitor'
|
||||
|
||||
@ -224,6 +247,7 @@ const userGroupRates = ref<Record<number, number>>({})
|
||||
interface MonitorForm {
|
||||
name: string
|
||||
provider: Provider
|
||||
api_mode: APIMode
|
||||
endpoint: string
|
||||
api_key: string
|
||||
primary_model: string
|
||||
@ -241,6 +265,7 @@ interface MonitorForm {
|
||||
const form = reactive<MonitorForm>({
|
||||
name: '',
|
||||
provider: PROVIDER_ANTHROPIC,
|
||||
api_mode: API_MODE_CHAT_COMPLETIONS,
|
||||
endpoint: '',
|
||||
api_key: '',
|
||||
primary_model: '',
|
||||
@ -254,15 +279,21 @@ const form = reactive<MonitorForm>({
|
||||
body_override: null,
|
||||
})
|
||||
|
||||
// 可用模板列表(进入 dialog 时一次性拉取 cache;按 provider 过滤)。
|
||||
let suppressFormWatchers = false
|
||||
|
||||
// 可用模板列表(进入 dialog 时一次性拉取 cache;按 provider / api mode 过滤)。
|
||||
const templatesCache = ref<ChannelMonitorTemplate[]>([])
|
||||
const templatesLoading = ref(false)
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
const items = templatesCache.value.filter((t) => t.provider === form.provider)
|
||||
const items = templatesCache.value.filter((t) => {
|
||||
if (t.provider !== form.provider) return false
|
||||
if (form.provider !== PROVIDER_OPENAI) return true
|
||||
return normalizeAPIMode(t.api_mode) === form.api_mode
|
||||
})
|
||||
return [
|
||||
{ value: '', label: t('admin.channelMonitor.templateField.none') },
|
||||
...items.map((t) => ({ value: String(t.id), label: t.name })),
|
||||
...items.map((t) => ({ value: String(t.id), label: templateOptionLabel(t) })),
|
||||
]
|
||||
})
|
||||
|
||||
@ -294,13 +325,57 @@ const templateSelectValue = computed<string>({
|
||||
// 应用模板 = 拷贝快照
|
||||
const tpl = templatesCache.value.find((t) => t.id === id)
|
||||
if (tpl) {
|
||||
suppressFormWatchers = true
|
||||
form.api_mode = normalizeAPIMode(tpl.api_mode)
|
||||
form.template_id = id
|
||||
form.extra_headers = { ...(tpl.extra_headers || {}) }
|
||||
form.body_override_mode = tpl.body_override_mode
|
||||
form.body_override = tpl.body_override ? { ...tpl.body_override } : null
|
||||
suppressFormWatchers = false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const apiModeOptions = computed<{ value: APIMode; label: string; hint: string }[]>(() => [
|
||||
{
|
||||
value: API_MODE_CHAT_COMPLETIONS,
|
||||
label: t('admin.channelMonitor.form.apiModeChatCompletions'),
|
||||
hint: t('admin.channelMonitor.form.apiModeChatCompletionsHint'),
|
||||
},
|
||||
{
|
||||
value: API_MODE_RESPONSES,
|
||||
label: t('admin.channelMonitor.form.apiModeResponses'),
|
||||
hint: t('admin.channelMonitor.form.apiModeResponsesHint'),
|
||||
},
|
||||
])
|
||||
|
||||
function normalizeAPIMode(mode: APIMode | undefined | null): APIMode {
|
||||
return mode === API_MODE_RESPONSES ? API_MODE_RESPONSES : API_MODE_CHAT_COMPLETIONS
|
||||
}
|
||||
|
||||
function apiModeButtonClass(mode: APIMode): string {
|
||||
const active = form.api_mode === mode
|
||||
if (active) {
|
||||
return 'border-primary-500 bg-white text-primary-700 shadow-sm dark:border-primary-400 dark:bg-primary-500/15 dark:text-primary-300'
|
||||
}
|
||||
return 'border-blue-100 bg-white/70 text-gray-600 hover:border-primary-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400'
|
||||
}
|
||||
|
||||
function templateOptionLabel(tpl: ChannelMonitorTemplate): string {
|
||||
if (tpl.provider !== PROVIDER_OPENAI) return tpl.name
|
||||
const labelKey = normalizeAPIMode(tpl.api_mode) === API_MODE_RESPONSES
|
||||
? 'admin.channelMonitor.form.apiModeResponses'
|
||||
: 'admin.channelMonitor.form.apiModeChatCompletions'
|
||||
return `${tpl.name} · ${t(labelKey)}`
|
||||
}
|
||||
|
||||
function clearRequestSnapshot() {
|
||||
form.template_id = null
|
||||
form.extra_headers = {}
|
||||
form.body_override_mode = 'off'
|
||||
form.body_override = null
|
||||
}
|
||||
|
||||
interface ProviderOption {
|
||||
value: Provider
|
||||
label: string
|
||||
@ -318,13 +393,26 @@ const providerOptions = computed<ProviderOption[]>(() => [
|
||||
// picks a new key.
|
||||
// 同时清空 template_id(模板有 provider 归属,跨平台不通用)。
|
||||
watch(() => form.provider, () => {
|
||||
if (suppressFormWatchers) return
|
||||
form.api_key = ''
|
||||
form.template_id = null
|
||||
})
|
||||
if (form.provider !== PROVIDER_OPENAI) {
|
||||
form.api_mode = API_MODE_CHAT_COMPLETIONS
|
||||
}
|
||||
clearRequestSnapshot()
|
||||
}, { flush: 'sync' })
|
||||
|
||||
watch(() => form.api_mode, () => {
|
||||
if (suppressFormWatchers) return
|
||||
if (form.provider === PROVIDER_OPENAI) {
|
||||
clearRequestSnapshot()
|
||||
}
|
||||
}, { flush: 'sync' })
|
||||
|
||||
function resetForm() {
|
||||
suppressFormWatchers = true
|
||||
form.name = ''
|
||||
form.provider = PROVIDER_ANTHROPIC
|
||||
form.api_mode = API_MODE_CHAT_COMPLETIONS
|
||||
form.endpoint = ''
|
||||
form.api_key = ''
|
||||
form.primary_model = ''
|
||||
@ -336,11 +424,14 @@ function resetForm() {
|
||||
form.extra_headers = {}
|
||||
form.body_override_mode = 'off'
|
||||
form.body_override = null
|
||||
suppressFormWatchers = false
|
||||
}
|
||||
|
||||
function loadFromMonitor(m: ChannelMonitor) {
|
||||
suppressFormWatchers = true
|
||||
form.name = m.name
|
||||
form.provider = m.provider
|
||||
form.api_mode = normalizeAPIMode(m.api_mode)
|
||||
form.endpoint = m.endpoint
|
||||
form.api_key = ''
|
||||
form.primary_model = m.primary_model
|
||||
@ -352,6 +443,7 @@ function loadFromMonitor(m: ChannelMonitor) {
|
||||
form.extra_headers = { ...(m.extra_headers || {}) }
|
||||
form.body_override_mode = m.body_override_mode || 'off'
|
||||
form.body_override = m.body_override ? { ...m.body_override } : null
|
||||
suppressFormWatchers = false
|
||||
}
|
||||
|
||||
// Re-sync form whenever the dialog is opened or the target monitor changes.
|
||||
@ -404,6 +496,7 @@ function buildPayload(): CreateParams {
|
||||
return {
|
||||
name: form.name.trim(),
|
||||
provider: form.provider,
|
||||
api_mode: form.provider === PROVIDER_OPENAI ? form.api_mode : API_MODE_CHAT_COMPLETIONS,
|
||||
endpoint: form.endpoint.trim(),
|
||||
api_key: form.api_key.trim(),
|
||||
primary_model: form.primary_model.trim(),
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
/>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ m.name }}</span>
|
||||
<span class="text-xs text-gray-400">{{ m.provider }}</span>
|
||||
<span v-if="m.provider === 'openai'" class="text-xs text-gray-400">{{ m.api_mode }}</span>
|
||||
<span
|
||||
v-if="!m.enabled"
|
||||
class="ml-auto rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 dark:bg-dark-700 dark:text-gray-400"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user