Files
openteam/web/src/views/admin/ConfigView.vue
T
SakurasanandClaude b365779188 模型限制: 系统配置全局开放/禁止 + 用户级限制 + 网关拦截
- User 新增 allowed_models/denied_models(用户级模型限制)
- 系统配置 model_allowlist/model_denylist 全局策略, 保存即时失效网关缓存
- 网关 checkModelAllowed: 用户级 > 全局(禁止命中→403, 白名单非空→仅白名单)
- 三个协议处理器均校验, 错误按客户端协议格式返回
- 配置页"模型限制"卡片(全局允许/禁止多选); 用户编辑支持允许/禁止模型

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-16 01:34:12 +08:00

165 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { http, errMsg } from '@/api/client'
import { useToastStore } from '@/stores/toast'
import Button from '@/components/ui/Button.vue'
const toast = useToastStore()
const config = reactive<Record<string, string>>({})
const loading = ref(false)
const saving = ref(false)
const availableModels = ref<string[]>([])
const allowList = ref<string[]>([])
const denyList = ref<string[]>([])
function parseConfigList(v: unknown): string[] {
if (!v) return []
if (Array.isArray(v)) return v.map(String)
try {
const a = JSON.parse(String(v))
return Array.isArray(a) ? a.map(String) : []
} catch {
return []
}
}
async function load() {
loading.value = true
try {
const { data } = await http.get('/admin/config')
Object.keys(config).forEach((k) => delete config[k])
Object.assign(config, data.data.config)
allowList.value = parseConfigList(config.model_allowlist)
denyList.value = parseConfigList(config.model_denylist)
delete config.model_allowlist
delete config.model_denylist
} catch (e) {
toast.err(errMsg(e))
} finally {
loading.value = false
}
}
async function loadModels() {
try {
const { data } = await http.get('/admin/models')
availableModels.value = (data.data.items as { name: string }[]).map((m) => m.name)
} catch {
/* 忽略 */
}
}
async function save() {
saving.value = true
try {
const payload: Record<string, unknown> = { ...config }
payload.model_allowlist = allowList.value
payload.model_denylist = denyList.value
await http.put('/admin/config', payload)
toast.ok('配置已保存')
} catch (e) {
toast.err(errMsg(e))
} finally {
saving.value = false
}
}
onMounted(() => {
load()
loadModels()
})
</script>
<template>
<div class="mx-auto max-w-2xl space-y-6">
<div class="mb-2">
<h1 class="text-lg font-semibold">系统配置</h1>
<p class="text-sm text-muted">注册策略与模型访问限制</p>
</div>
<div class="card p-6">
<div class="mb-5">
<h2 class="text-sm font-semibold">模型限制</h2>
<p class="mt-1 text-xs text-muted">
针对全部用户开放/禁止模型;用户级限制优先级更高(用户管理里可单独配置)。
</p>
</div>
<div class="space-y-5">
<div>
<p class="mb-2 text-xs font-medium text-muted">允许的模型(留空 = 全部开放)</p>
<div class="flex max-h-40 flex-wrap gap-2 overflow-y-auto">
<label
v-for="m in availableModels"
:key="m"
class="flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs transition select-none"
:class="allowList.includes(m) ? 'border-accent bg-accent-soft text-ink' : 'border-edge2 text-muted hover:border-edge'"
>
<input v-model="allowList" type="checkbox" :value="m" class="size-3.5 rounded accent-[var(--color-accent)]" />
{{ m }}
</label>
<p v-if="availableModels.length === 0" class="text-xs text-muted">暂无模型,请先在模型定价中添加</p>
</div>
</div>
<div>
<p class="mb-2 text-xs font-medium text-muted">禁止的模型(黑名单优先)</p>
<div class="flex max-h-40 flex-wrap gap-2 overflow-y-auto">
<label
v-for="m in availableModels"
:key="m"
class="flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs transition select-none"
:class="denyList.includes(m) ? 'border-err bg-err-soft text-err' : 'border-edge2 text-muted hover:border-edge'"
>
<input v-model="denyList" type="checkbox" :value="m" class="size-3.5 rounded accent-[var(--color-err)]" />
{{ m }}
</label>
</div>
</div>
</div>
</div>
<div class="card space-y-5 p-6">
<div v-if="loading" class="py-8 text-center text-sm text-muted">加载中…</div>
<template v-else>
<div class="space-y-4">
<div>
<p class="mb-1.5 text-xs font-medium text-muted">注册模式</p>
<div class="flex gap-2">
<button
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
:class="config.registration_mode === 'open' ? 'border-accent bg-accent-soft text-accent' : 'border-edge2 text-muted'"
@click="config.registration_mode = 'open'"
>
开放注册
</button>
<button
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
:class="config.registration_mode === 'invite' ? 'border-accent bg-accent-soft text-accent' : 'border-edge2 text-muted'"
@click="config.registration_mode = 'invite'"
>
邀请码
</button>
</div>
<p class="mt-1.5 text-xs text-muted">邀请模式下注册需填写有效邀请码(invite_codes 配置)</p>
</div>
<div>
<p class="mb-1.5 text-xs font-medium text-muted">邀请码(逗号分隔)</p>
<input
v-model="config.invite_codes"
placeholder="code1,code2"
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 font-mono text-xs outline-none focus:border-accent"
/>
</div>
</div>
</template>
</div>
<div class="flex justify-end">
<Button :loading="saving" @click="save">保存配置</Button>
</div>
</div>
</template>