feat: 完善人工关注与内容合规配置
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from "element-plus";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { api } from "../services/api";
|
||||
import type { AttentionConfig, AttentionPreviewMessage, AttentionPreviewResult } from "../types/api";
|
||||
import AdminPagination from "./AdminPagination.vue";
|
||||
|
||||
const props = defineProps<{ canConfig: boolean; canPreview: boolean }>();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const previewLoading = ref(false);
|
||||
const previewMessages = ref<AttentionPreviewMessage[]>([]);
|
||||
const selectedMessages = ref<AttentionPreviewMessage[]>([]);
|
||||
const previewResults = ref<AttentionPreviewResult[]>([]);
|
||||
const keyword = ref("");
|
||||
const pager = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
const terms = reactive({ urgent: "", important: "", normal: "" });
|
||||
const config = reactive<AttentionConfig>({
|
||||
enabled: true,
|
||||
keywordEnabled: true,
|
||||
aiEnabled: false,
|
||||
knowledgeMissingEnabled: true,
|
||||
urgentTerms: [],
|
||||
importantTerms: [],
|
||||
normalTerms: [],
|
||||
promptTemplate: "",
|
||||
recognitionItems: [],
|
||||
});
|
||||
|
||||
const canRunPreview = computed(() => props.canPreview && selectedMessages.value.length > 0 && !previewLoading.value);
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.canConfig || props.canPreview) await loadConfig();
|
||||
if (props.canPreview) await loadPreviewMessages();
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true;
|
||||
try {
|
||||
assignConfig(props.canConfig ? await api.attentionConfig() : await api.attentionPreviewConfig());
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function assignConfig(value: AttentionConfig) {
|
||||
Object.assign(config, value);
|
||||
terms.urgent = value.urgentTerms.join("\n");
|
||||
terms.important = value.importantTerms.join("\n");
|
||||
terms.normal = value.normalTerms.join("\n");
|
||||
}
|
||||
|
||||
function currentConfig(): AttentionConfig {
|
||||
return {
|
||||
...config,
|
||||
urgentTerms: splitTerms(terms.urgent),
|
||||
importantTerms: splitTerms(terms.important),
|
||||
normalTerms: splitTerms(terms.normal),
|
||||
};
|
||||
}
|
||||
|
||||
function splitTerms(value: string) {
|
||||
return [...new Set(value.split(/[\n,,]+/).map((item) => item.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function addRecognitionItem() {
|
||||
if (config.recognitionItems.length >= 30) {
|
||||
ElMessage.warning("可识别项最多配置 30 个");
|
||||
return;
|
||||
}
|
||||
config.recognitionItems.push({ name: "", description: "", priority: "normal", enabled: true });
|
||||
}
|
||||
|
||||
function removeRecognitionItem(index: number) {
|
||||
config.recognitionItems.splice(index, 1);
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
saving.value = true;
|
||||
try {
|
||||
assignConfig(await api.saveAttentionConfig(currentConfig()));
|
||||
ElMessage.success("人工关注筛选规则已保存,后续新问答立即生效");
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "配置保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviewMessages(page = 1, pageSize = pager.pageSize) {
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const result = await api.attentionPreviewMessages({ keyword: keyword.value.trim(), page, pageSize });
|
||||
previewMessages.value = result.items;
|
||||
selectedMessages.value = [];
|
||||
Object.assign(pager, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: AttentionPreviewMessage[]) {
|
||||
if (rows.length > 20) {
|
||||
ElMessage.warning("单次最多预览 20 条历史消息");
|
||||
selectedMessages.value = rows.slice(0, 20);
|
||||
return;
|
||||
}
|
||||
selectedMessages.value = rows;
|
||||
}
|
||||
|
||||
async function runPreview() {
|
||||
if (!canRunPreview.value) return;
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
previewResults.value = await api.previewAttention(
|
||||
selectedMessages.value.map((item) => item.messageId),
|
||||
currentConfig(),
|
||||
);
|
||||
ElMessage.success(`已完成 ${previewResults.value.length} 条历史消息筛选预览`);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "提示词筛选预览失败");
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function priorityLabel(value: string) {
|
||||
return ({ urgent: "紧急", important: "重要", normal: "普通" } as Record<string, string>)[value] || "-";
|
||||
}
|
||||
|
||||
function sourceLabel(value: string) {
|
||||
return ({ keyword: "关键词", knowledge_missing: "知识缺失", ai: "AI 提示词", rules: "未触发", disabled: "总开关关闭" } as Record<string, string>)[value] || value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="attention-config-workspace">
|
||||
<el-alert
|
||||
title="运行顺序:紧急/重要/普通关键词 → 知识缺失 → AI 提示词。确定性规则命中后不再调用模型。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<article v-if="canConfig" class="attention-config-card">
|
||||
<header><div><h3>自动筛选规则</h3><p>配置只影响保存后的新问答,不会自动重跑已有记录。</p></div><el-button type="primary" :loading="saving" @click="saveConfig">保存规则</el-button></header>
|
||||
<div class="attention-switch-grid">
|
||||
<label><span><strong>启用人工关注自动筛选</strong><small>关闭后不再自动创建新的关注记录</small></span><el-switch v-model="config.enabled" /></label>
|
||||
<label><span><strong>启用关键词筛选</strong><small>优先执行,不产生模型调用费用</small></span><el-switch v-model="config.keywordEnabled" /></label>
|
||||
<label><span><strong>知识缺失自动关注</strong><small>知识库应答缺少可靠正式知识时触发</small></span><el-switch v-model="config.knowledgeMissingEnabled" /></label>
|
||||
<label><span><strong>启用 AI 提示词筛选</strong><small>仅在前面规则未命中时异步执行,不会拖慢用户收到回答</small></span><el-switch v-model="config.aiEnabled" /></label>
|
||||
</div>
|
||||
<div class="attention-term-grid">
|
||||
<label><strong>紧急关键词</strong><small>每行一个,命中后标记为紧急</small><el-input v-model="terms.urgent" type="textarea" :rows="6" /></label>
|
||||
<label><strong>重要关键词</strong><small>每行一个,命中后标记为重要</small><el-input v-model="terms.important" type="textarea" :rows="6" /></label>
|
||||
<label><strong>普通关键词</strong><small>每行一个,命中后标记为普通</small><el-input v-model="terms.normal" type="textarea" :rows="6" /></label>
|
||||
</div>
|
||||
<section class="recognition-item-editor">
|
||||
<header><span><strong>AI 可识别项</strong><small>语义筛选只会从已启用项目中选择;可按业务需要继续增加。</small></span><el-button @click="addRecognitionItem">新增可识别项</el-button></header>
|
||||
<el-table :data="config.recognitionItems" border>
|
||||
<el-table-column label="启用" width="72"><template #default="{ row }"><el-switch v-model="row.enabled" /></template></el-table-column>
|
||||
<el-table-column label="项目名称" width="180"><template #default="{ row }"><el-input v-model="row.name" maxlength="50" placeholder="例如:疑似投诉" /></template></el-table-column>
|
||||
<el-table-column label="识别说明" min-width="360"><template #default="{ row }"><el-input v-model="row.description" maxlength="500" placeholder="说明什么情况下算命中,尽量写清边界" /></template></el-table-column>
|
||||
<el-table-column label="优先级" width="130"><template #default="{ row }"><el-select v-model="row.priority"><el-option label="紧急" value="urgent" /><el-option label="重要" value="important" /><el-option label="普通" value="normal" /></el-select></template></el-table-column>
|
||||
<el-table-column label="操作" width="80"><template #default="{ $index }"><el-button link type="danger" @click="removeRecognitionItem($index)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<label class="attention-prompt-editor">
|
||||
<span><strong>AI 筛选提示词</strong><small>系统会自动追加历史问答数据、安全边界和固定 JSON 输出格式,无需在提示词中写变量。</small></span>
|
||||
<el-input v-model="config.promptTemplate" type="textarea" :rows="12" maxlength="8000" show-word-limit />
|
||||
</label>
|
||||
</article>
|
||||
|
||||
<article v-if="canPreview" class="attention-config-card">
|
||||
<header><div><h3>历史消息筛选预览</h3><p>可使用当前页面尚未保存的关键词和提示词运行,不会创建人工关注记录。</p></div><el-button type="primary" :loading="previewLoading" :disabled="!canRunPreview" @click="runPreview">预览选中消息</el-button></header>
|
||||
<div class="attention-preview-search"><el-input v-model="keyword" clearable placeholder="搜索历史问题、用户姓名或手机号" @keyup.enter="loadPreviewMessages(1)" /><el-button @click="loadPreviewMessages(1)">查询</el-button><span>已选 {{ selectedMessages.length }}/20</span></div>
|
||||
<el-table :data="previewMessages" stripe @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="question" label="历史问题" min-width="280" show-overflow-tooltip />
|
||||
<el-table-column prop="userName" label="用户" width="120" />
|
||||
<el-table-column prop="sessionTitle" label="会话" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="知识缺失" width="100"><template #default="{ row }"><el-tag :type="row.knowledgeMissing ? 'warning' : 'info'">{{ row.knowledgeMissing ? "是" : "否" }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="createdAt" label="提问时间" width="180" />
|
||||
</el-table>
|
||||
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="loadPreviewMessages" />
|
||||
</article>
|
||||
|
||||
<article v-if="previewResults.length" class="attention-config-card preview-result-card">
|
||||
<header><div><h3>筛选结果</h3><p>结果仅供调整规则和提示词,不会写入关注记录。</p></div></header>
|
||||
<el-table :data="previewResults" stripe>
|
||||
<el-table-column label="结果" width="100"><template #default="{ row }"><el-tag :type="row.needsAttention ? 'danger' : 'success'">{{ row.needsAttention ? "需关注" : "不关注" }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="优先级" width="90"><template #default="{ row }">{{ priorityLabel(row.priority) }}</template></el-table-column>
|
||||
<el-table-column label="来源" width="110"><template #default="{ row }">{{ sourceLabel(row.source) }}</template></el-table-column>
|
||||
<el-table-column prop="matchedItem" label="命中项目" width="140" />
|
||||
<el-table-column prop="question" label="历史问题" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="reason" label="判断理由" min-width="220" />
|
||||
<el-table-column prop="summary" label="问题摘要" min-width="220" />
|
||||
<el-table-column label="详情" width="100"><template #default="{ row }"><el-popover placement="left" :width="560" trigger="click"><template #reference><el-button link type="primary">提示词/原始输出</el-button></template><div class="attention-preview-detail"><strong>最终提示词</strong><pre>{{ row.renderedPrompt || '关键词规则命中,未调用 AI' }}</pre><strong>模型原始输出</strong><pre>{{ row.rawOutput || '无' }}</pre></div></el-popover></template></el-table-column>
|
||||
</el-table>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.attention-config-workspace { display: grid; gap: 18px; }
|
||||
.attention-config-card { padding: 20px; border: 1px solid #e3e9e7; border-radius: 14px; background: #fff; }
|
||||
.attention-config-card > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
|
||||
.attention-config-card h3 { margin: 0; color: #1f2d29; }
|
||||
.attention-config-card p { margin: 6px 0 0; color: #73817c; font-size: 13px; }
|
||||
.attention-switch-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.attention-switch-grid label { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px; border: 1px solid #e6ece9; border-radius: 10px; }
|
||||
.attention-switch-grid span, .attention-prompt-editor > span { display: grid; gap: 5px; }
|
||||
.attention-switch-grid small, .attention-term-grid small, .attention-prompt-editor small { color: #82908b; font-weight: 400; }
|
||||
.attention-term-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 16px; }
|
||||
.attention-term-grid label { display: grid; gap: 7px; }
|
||||
.attention-prompt-editor { display: grid; gap: 10px; margin-top: 18px; }
|
||||
.recognition-item-editor { display: grid; gap: 10px; margin-top: 18px; }
|
||||
.recognition-item-editor > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.recognition-item-editor > header span { display: grid; gap: 5px; }
|
||||
.recognition-item-editor small { color: #82908b; font-weight: 400; }
|
||||
.attention-preview-search { display: grid; grid-template-columns: minmax(260px, 520px) auto 1fr; align-items: center; gap: 10px; margin-bottom: 14px; }
|
||||
.attention-preview-search span { color: #73817c; font-size: 13px; }
|
||||
.attention-preview-detail { display: grid; gap: 8px; }
|
||||
.attention-preview-detail pre { max-height: 300px; margin: 0 0 10px; padding: 10px; overflow: auto; border-radius: 8px; background: #f6f8f7; white-space: pre-wrap; word-break: break-word; }
|
||||
@media (max-width: 980px) { .attention-switch-grid, .attention-term-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user