diff --git a/CHANGELOG.md b/CHANGELOG.md index 791a7de..34cda95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。 +## [0.2.0] - 2026-07-12 + +### Added + +- AI 网关页「模型黑名单」管理:模型列表按厂商手风琴分组展示,可一键拉黑(全渠道剔除)、移除后重新同步渠道恢复 +- AI 密钥模型白名单编辑:创建 / 编辑密钥时可限定放行的模型列表(空 = 不限) +- 渠道 / 密钥表单「现有分组」chips:点击即填入分组输入框 + +### Changed + +- 租户删除确认弹窗分段排版(标题 / 面板清理范围 / 云端提示),列表与详情页文案一致 + ## [0.1.0] - 2026-07-10 ### Added diff --git a/package.json b/package.json index 411e0ac..563e8a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oci-portal-dash", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/api/aigateway.ts b/src/api/aigateway.ts index 5d0a442..845a0e7 100644 --- a/src/api/aigateway.ts +++ b/src/api/aigateway.ts @@ -1,5 +1,13 @@ import { request } from './request' -import type { AiCallLog, AiChannel, AiContentLog, AiKey, AiModel, AiModelCacheItem } from '@/types/api' +import type { + AiCallLog, + AiChannel, + AiContentLog, + AiKey, + AiModel, + AiModelBlacklistItem, + AiModelCacheItem, +} from '@/types/api' // ---- 网关密钥 ---- @@ -11,6 +19,8 @@ export interface CreateAiKeyRequest { name: string value?: string group?: string + /** 模型白名单;缺省/空 = 不限 */ + models?: string[] } /** 创建密钥;key 为明文,仅本次响应返回 */ @@ -20,7 +30,7 @@ export function createAiKey(body: CreateAiKeyRequest): Promise<{ key: string; it export function updateAiKey( id: number, - body: { name?: string; enabled?: boolean; group?: string }, + body: { name?: string; enabled?: boolean; group?: string; models?: string[] }, ): Promise { return request(`/ai-keys/${id}`, { method: 'PUT', body }) } @@ -78,6 +88,22 @@ export function listAiModels(): Promise { return request<{ items: AiModel[] }>('/ai-models').then((r) => r.items) } +// ---- 模型黑名单 ---- + +export function listAiBlacklist(): Promise { + return request<{ items: AiModelBlacklistItem[] }>('/ai-blacklist').then((r) => r.items) +} + +/** 拉黑模型:删除全部渠道缓存中的同名条目,后续同步 / 探测均过滤 */ +export function addAiBlacklist(name: string): Promise { + return request('/ai-blacklist', { method: 'POST', body: { name } }) +} + +/** 移出黑名单:缓存不回填,重新同步 / 探测后恢复入池 */ +export function removeAiBlacklist(id: number): Promise { + return request(`/ai-blacklist/${id}`, { method: 'DELETE' }) +} + export function listAiCallLogs(params: { page: number size: number diff --git a/src/components/ai/AiChannelPanel.vue b/src/components/ai/AiChannelPanel.vue index a718b51..c907e70 100644 --- a/src/components/ai/AiChannelPanel.vue +++ b/src/components/ai/AiChannelPanel.vue @@ -4,6 +4,7 @@ import { computed, h, reactive, ref } from 'vue' import { createAiChannel, deleteAiChannel, probeAiChannel, syncAiChannelModels, updateAiChannel } from '@/api/aigateway' import { listCachedRegions } from '@/api/configs' +import GroupChips from '@/components/ai/GroupChips.vue' import AppInputNumber from '@/components/AppInputNumber.vue' import FormField from '@/components/FormField.vue' import FormModal from '@/components/FormModal.vue' @@ -15,9 +16,11 @@ import { useToast } from '@/composables/useToast' import { useScopeStore } from '@/stores/scope' import type { AiChannel, AiProbeStatus } from '@/types/api' -defineProps<{ channels: AiChannel[]; loading: boolean }>() +const props = defineProps<{ channels: AiChannel[]; loading: boolean }>() const emit = defineEmits<{ changed: [] }>() +const groupHints = computed(() => [...new Set(props.channels.map((c) => c.group).filter(Boolean))]) + const toast = useToast() const dialog = useDialog() const scope = useScopeStore() @@ -233,6 +236,7 @@ const columns = computed>(() => [
+ diff --git a/src/components/ai/AiKeyPanel.vue b/src/components/ai/AiKeyPanel.vue index d41a584..8ef2ed0 100644 --- a/src/components/ai/AiKeyPanel.vue +++ b/src/components/ai/AiKeyPanel.vue @@ -1,17 +1,18 @@ + + diff --git a/src/types/api.ts b/src/types/api.ts index 536e1d0..afb91ee 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -946,6 +946,8 @@ export interface AiKey { name: string tail: string group: string + /** 模型白名单;null/空 = 不限,非空时网关仅放行列表内模型 */ + models: string[] | null enabled: boolean lastUsedAt: string | null /** 内容日志开启截止时间;null = 永关(默认,红线),开启必须限时到期自动停写 */ @@ -991,6 +993,13 @@ export interface AiModelCacheItem { syncedAt: string } +/** 模型黑名单条目:拉黑即全渠道剔除,同步 / 探测不再入库 */ +export interface AiModelBlacklistItem { + id: number + name: string + createdAt: string +} + /** 调用日志:仅元数据与 token 用量 */ export interface AiCallLog { id: number diff --git a/src/views/AiGatewayView.vue b/src/views/AiGatewayView.vue index 7e206b0..6b64032 100644 --- a/src/views/AiGatewayView.vue +++ b/src/views/AiGatewayView.vue @@ -1,23 +1,32 @@ @@ -114,7 +160,7 @@ function refreshChannels() {
可用模型
-
按厂商分组;点击厂商展开,点击模型名复制
+
按厂商分组;点击厂商展开,点击模型名复制,✕ 加入黑名单
{{ models.data.value?.length ?? '…' }} 个
@@ -144,16 +190,67 @@ function refreshChannels() { v-if="expandedVendor === g.vendor" class="flex max-h-36 flex-wrap content-start gap-1.5 overflow-y-auto pb-2 pl-5" > - + + + +
+ +
+ +
+
同步与探测均过滤;点击移出,重新同步渠道后恢复入池
+
+ +
@@ -170,6 +267,7 @@ function refreshChannels() { :keys="keys.data.value ?? []" :loading="keys.loading.value" :groups="groups" + :models="models.data.value ?? []" @changed="keys.run({ silent: true })" /> diff --git a/src/views/TenantDetailView.vue b/src/views/TenantDetailView.vue index 9056929..5732469 100644 --- a/src/views/TenantDetailView.vue +++ b/src/views/TenantDetailView.vue @@ -140,8 +140,15 @@ async function remove() { - 删除「{{ config.data.value.alias }}」?将清理本面板中的关联任务、快照、回传事件与 Webhook - 密钥、告警及 AI 渠道数据;OCI 云端资源不会被删除(云端日志回传链路请先在“其他”页销毁)。 +
+
删除「{{ config.data.value.alias }}」?
+
+ 将清理面板内关联数据:后台任务、测活 / 成本快照、回传事件、Webhook 密钥、告警规则与 AI 渠道。 +
+
+ OCI 云端资源不受影响;云端日志回传链路请先在「其他」页销毁。 +
+
diff --git a/src/views/TenantListView.vue b/src/views/TenantListView.vue index 2be514b..cf475c0 100644 --- a/src/views/TenantListView.vue +++ b/src/views/TenantListView.vue @@ -222,7 +222,19 @@ function renderActions(row: OciConfigSummary): VNodeChild { trigger: () => h(NButton, { size: 'tiny', quaternary: true, type: 'error' }, { default: () => '删除' }), default: () => - `删除「${row.alias}」?将清理本面板中的关联任务、快照、回传事件与 Webhook 密钥、告警及 AI 渠道数据;OCI 云端资源不会被删除(云端日志回传链路请先在“其他”页销毁)。`, + h('div', { class: 'max-w-80 text-[13px] leading-relaxed' }, [ + h('div', { class: 'font-medium' }, `删除「${row.alias}」?`), + h( + 'div', + { class: 'mt-1 text-xs text-ink-3' }, + '将清理面板内关联数据:后台任务、测活 / 成本快照、回传事件、Webhook 密钥、告警规则与 AI 渠道。', + ), + h( + 'div', + { class: 'mt-0.5 text-xs text-ink-3' }, + 'OCI 云端资源不受影响;云端日志回传链路请先在「其他」页销毁。', + ), + ]), }, ), ])