feat(risk-control): record matched keyword in keyword-block logs

The risk control center's moderation log records had no field for the
keyword that triggered a keyword block, so the admin UI couldn't show
which keyword was hit (only the application slog logged it).

- migration 156: add matched_keyword column to content_moderation_logs
- ContentModerationLog gains MatchedKeyword; set it on keyword block
- repo CreateLog/ListLogs persist and read the column
- frontend: show "命中关键词" inline in the log table and detail modal
- i18n: add matchedKeyword (zh/en)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bwlc
2026-06-26 10:43:05 +08:00
co-authored by Claude Opus 4.8
parent e5f38a6f60
commit 815bc6c9b5
8 changed files with 21 additions and 4 deletions
+1
View File
@@ -182,6 +182,7 @@ export interface ContentModerationLog {
flagged: boolean
highest_category: string
highest_score: number
matched_keyword: string
category_scores: Record<string, number>
threshold_snapshot: Record<string, number>
input_excerpt: string
+1
View File
@@ -2648,6 +2648,7 @@ export default {
unbanFailed: 'Failed to unban user',
inputDetailTitle: 'Input Summary Detail',
inputDetailContent: 'Full Content',
matchedKeyword: 'Matched Keyword',
queueDelay: 'Queued {ms} ms',
allGroups: 'All Groups',
allGroupsHint: 'Auditing all groups',
+1
View File
@@ -2725,6 +2725,7 @@ export default {
unbanFailed: '解封用户失败',
inputDetailTitle: '输入摘要详情',
inputDetailContent: '完整内容',
matchedKeyword: '命中关键词',
queueDelay: '排队 {ms} ms',
allGroups: '全部分组',
allGroupsHint: '当前审计全部分组',
@@ -317,6 +317,9 @@
<td class="whitespace-nowrap px-5 py-4 text-sm text-gray-700 dark:text-gray-300">
<div>{{ row.highest_category || '-' }}</div>
<div class="text-xs text-gray-400">{{ percent(row.highest_score) }}</div>
<div v-if="row.matched_keyword" class="mt-0.5 text-xs font-medium text-red-600 dark:text-red-300" :title="t('admin.riskControl.matchedKeyword') + ': ' + row.matched_keyword">
{{ t('admin.riskControl.matchedKeyword') }}: {{ row.matched_keyword }}
</div>
</td>
<td class="whitespace-nowrap px-5 py-4 text-sm text-gray-700 dark:text-gray-300">
<div>{{ violationCountText(row) }}</div>
@@ -1078,6 +1081,10 @@
{{ inputDetailRow.highest_category || '-' }} / {{ percent(inputDetailRow.highest_score) }}
</p>
</div>
<div v-if="inputDetailRow.matched_keyword" class="rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-900/60 dark:bg-red-900/20">
<p class="text-xs font-medium text-red-500 dark:text-red-300">{{ t('admin.riskControl.matchedKeyword') }}</p>
<p class="mt-1 truncate text-sm font-semibold text-red-700 dark:text-red-200" :title="inputDetailRow.matched_keyword">{{ inputDetailRow.matched_keyword }}</p>
</div>
</div>
<div class="rounded-xl border border-gray-100 bg-white p-4 shadow-sm dark:border-dark-700 dark:bg-dark-800">