-
{{ row.masked || '-' }}
-
- {{ row.configured ? t('admin.riskControl.apiKeyConfigured') : t('admin.riskControl.apiKeyTemporary') }}
-
+
+
+
+
+
+
+ {{ row.masked || '-' }}
+
+ {{ isStoredApiKeyPendingDelete(row) ? t('admin.riskControl.apiKeyPendingDelete') : row.configured ? t('admin.riskControl.apiKeyConfigured') : t('admin.riskControl.apiKeyTemporary') }}
+
+
+
{{ apiKeyStatusMeta(row) }}
+
+
+
+
+ {{ apiKeyStatusLabel(row.status) }}
+
+
-
{{ apiKeyStatusMeta(row) }}
-
-
- {{ apiKeyStatusLabel(row.status) }}
-
+
+ {{ row.last_error }}
+
-
- {{ row.last_error }}
-
+
+
+
+
+ {{ apiKeyRowsExpanded ? t('admin.riskControl.apiKeyRowsExpanded', { count: apiKeyRows.length }) : t('admin.riskControl.apiKeyRowsCollapsed', { count: hiddenApiKeyRowCount }) }}
+
+
@@ -780,6 +840,7 @@ import { formatDateTime as formatDateTimeValue } from '@/utils/format'
type SettingsTab = 'basic' | 'scope' | 'runtime' | 'response' | 'retention'
type WorkerSlotState = 'active' | 'idle' | 'disabled'
+type APIKeysWriteMode = 'append' | 'replace'
type OverviewIcon = 'shield' | 'key' | 'users' | 'document'
type OverviewItem = {
key: string
@@ -798,8 +859,9 @@ type ModerationScoreRow = {
hit: boolean
}
-const maxModerationTestImages = 4
+const maxModerationTestImages = 1
const maxModerationTestImageSize = 8 * 1024 * 1024
+const maxVisibleApiKeyRows: number = 3
const { t } = useI18n()
const appStore = useAppStore()
@@ -819,6 +881,8 @@ const groups = ref
([])
const logs = ref([])
const status = ref(null)
const testedApiKeyStatuses = ref([])
+const pendingDeleteApiKeyHashes = ref([])
+const apiKeyRowsExpanded = ref(false)
const moderationTestPrompt = ref('')
const moderationTestImages = ref([])
const moderationTestResult = ref(null)
@@ -836,6 +900,7 @@ const configForm = reactive({
api_key_count: 0,
api_key_masks: [] as string[],
api_key_statuses: [] as ContentModerationAPIKeyStatus[],
+ api_keys_mode: 'append' as APIKeysWriteMode,
clear_api_key: false,
timeout_ms: 3000,
retry_count: 2,
@@ -922,14 +987,24 @@ const filteredGroups = computed(() => {
})
})
-const apiKeyPlaceholder = computed(() => {
- if (configForm.clear_api_key) return t('admin.riskControl.apiKeyWillClear')
- if (configForm.api_key_configured) return t('admin.riskControl.apiKeysPlaceholderKeep')
- return t('admin.riskControl.apiKeysPlaceholder')
-})
-
const inputApiKeyCount = computed(() => parseApiKeys(configForm.api_keys_text).length)
+const pendingDeletedApiKeyCount = computed(() => pendingDeleteApiKeyHashes.value.length)
+
+const effectiveStoredApiKeyCount = computed(() => Math.max(0, configForm.api_key_count - pendingDeletedApiKeyCount.value))
+
+const apiKeysPlaceholder = computed(() => (
+ configForm.api_keys_mode === 'replace'
+ ? t('admin.riskControl.apiKeysPlaceholderReplace')
+ : t('admin.riskControl.apiKeysPlaceholder')
+))
+
+const apiKeysModeHint = computed(() => (
+ configForm.api_keys_mode === 'replace'
+ ? t('admin.riskControl.apiKeysModeReplaceHint')
+ : t('admin.riskControl.apiKeysModeAppendHint')
+))
+
const hasModerationAuditInput = computed(() => {
return moderationTestPrompt.value.trim() !== '' || moderationTestImages.value.length > 0
})
@@ -954,6 +1029,19 @@ const apiKeyRows = computed(() => [
...testedApiKeyStatuses.value,
])
+const visibleApiKeyRows = computed(() => {
+ if (apiKeyRowsExpanded.value) return apiKeyRows.value
+ return apiKeyRows.value.slice(0, maxVisibleApiKeyRows)
+})
+
+const hiddenApiKeyRowCount = computed(() => Math.max(0, apiKeyRows.value.length - visibleApiKeyRows.value.length))
+
+const canToggleApiKeyRows = computed(() => apiKeyRows.value.length > maxVisibleApiKeyRows)
+
+const activeSavedApiKeyRows = computed(() => (
+ savedApiKeyRows.value.filter((row) => !isStoredApiKeyPendingDelete(row))
+))
+
const apiKeyHealthBadges = computed>(() => {
const counts: Record = {
ok: 0,
@@ -961,11 +1049,11 @@ const apiKeyHealthBadges = computed 0) {
- counts.unknown = configForm.api_key_count
+ if (activeSavedApiKeyRows.value.length === 0 && effectiveStoredApiKeyCount.value > 0) {
+ counts.unknown = effectiveStoredApiKeyCount.value
}
return (['ok', 'frozen', 'error', 'unknown'] as Array)
.map((item) => ({ status: item, count: counts[item] }))
@@ -1085,8 +1173,11 @@ function applyConfig(config: ContentModerationConfig) {
configForm.api_key_count = config.api_key_count || 0
configForm.api_key_masks = Array.isArray(config.api_key_masks) ? [...config.api_key_masks] : []
configForm.api_key_statuses = Array.isArray(config.api_key_statuses) ? [...config.api_key_statuses] : []
+ configForm.api_keys_mode = 'append'
configForm.clear_api_key = false
+ pendingDeleteApiKeyHashes.value = []
testedApiKeyStatuses.value = []
+ apiKeyRowsExpanded.value = false
configForm.timeout_ms = config.timeout_ms || 3000
configForm.retry_count = config.retry_count ?? 2
configForm.sample_rate = config.sample_rate ?? 100
@@ -1119,6 +1210,7 @@ async function loadAll() {
status.value = runtimeStatus
if (Array.isArray(runtimeStatus.api_key_statuses)) {
configForm.api_key_statuses = [...runtimeStatus.api_key_statuses]
+ prunePendingDeleteAPIKeyHashes()
}
await loadLogs()
} catch (err: unknown) {
@@ -1135,6 +1227,7 @@ async function loadStatus(silent = true) {
status.value = runtimeStatus
if (Array.isArray(runtimeStatus.api_key_statuses)) {
configForm.api_key_statuses = [...runtimeStatus.api_key_statuses]
+ prunePendingDeleteAPIKeyHashes()
}
} catch (err: unknown) {
if (!silent) {
@@ -1173,10 +1266,18 @@ async function saveConfig() {
pre_hash_check_enabled: configForm.pre_hash_check_enabled,
}
const keys = parseApiKeys(configForm.api_keys_text)
+ if (!payload.clear_api_key && configForm.api_keys_mode === 'replace' && keys.length === 0) {
+ appStore.showError(t('admin.riskControl.apiKeysReplaceNoInput'))
+ return
+ }
if (keys.length > 0) {
payload.api_keys = keys
+ payload.api_keys_mode = configForm.api_keys_mode
payload.clear_api_key = false
}
+ if (!payload.clear_api_key && configForm.api_keys_mode !== 'replace' && pendingDeleteApiKeyHashes.value.length > 0) {
+ payload.delete_api_key_hashes = [...pendingDeleteApiKeyHashes.value]
+ }
const updated = await adminAPI.riskControl.updateConfig(payload)
applyConfig(updated)
@@ -1305,7 +1406,16 @@ function toggleClearApiKey() {
configForm.clear_api_key = !configForm.clear_api_key
if (configForm.clear_api_key) {
configForm.api_keys_text = ''
+ configForm.api_keys_mode = 'append'
testedApiKeyStatuses.value = []
+ pendingDeleteApiKeyHashes.value = []
+ }
+}
+
+function setAPIKeysMode(mode: APIKeysWriteMode) {
+ configForm.api_keys_mode = mode
+ if (mode === 'replace') {
+ pendingDeleteApiKeyHashes.value = []
}
}
@@ -1350,6 +1460,25 @@ function mergeConfiguredAPIKeyStatuses(items: ContentModerationAPIKeyStatus[]) {
configForm.api_key_statuses = configForm.api_key_statuses.map((item) => updates.get(item.key_hash) ?? item)
}
+function toggleDeleteStoredApiKey(row: ContentModerationAPIKeyStatus) {
+ if (!row.configured || !row.key_hash) return
+ const index = pendingDeleteApiKeyHashes.value.indexOf(row.key_hash)
+ if (index >= 0) {
+ pendingDeleteApiKeyHashes.value.splice(index, 1)
+ return
+ }
+ pendingDeleteApiKeyHashes.value.push(row.key_hash)
+}
+
+function isStoredApiKeyPendingDelete(row: ContentModerationAPIKeyStatus): boolean {
+ return row.configured && row.key_hash !== '' && pendingDeleteApiKeyHashes.value.includes(row.key_hash)
+}
+
+function prunePendingDeleteAPIKeyHashes() {
+ const currentHashes = new Set(savedApiKeyRows.value.map((row) => row.key_hash).filter(Boolean))
+ pendingDeleteApiKeyHashes.value = pendingDeleteApiKeyHashes.value.filter((hash) => currentHashes.has(hash))
+}
+
function clearModerationTestInput() {
moderationTestPrompt.value = ''
moderationTestImages.value = []