feat: restore learning feedback and knowledge capabilities

This commit is contained in:
2026-07-18 00:50:11 +08:00
parent 876a23a123
commit 6bdc86e3fc
152 changed files with 30337 additions and 953 deletions
+166
View File
@@ -0,0 +1,166 @@
<template>
<div class="web-search-page">
<section class="page-head">
<div>
<h1>全网 AI 配置</h1>
<p>独立配置公开互联网检索,不与企业 SOP 知识库混用。仅启用的 HTTPS 提供方会出现在员工端,启用前应先完成连接测试。</p>
</div>
<el-button type="danger" @click="openEditor()">新增提供方</el-button>
</section>
<el-alert
type="warning"
:closable="false"
show-icon
title="外部服务边界"
description="只允许 HTTPS 公网接口;密钥不会回显。员工查询会显示互联网来源提示,结果不能替代公司 SOP 或正式制度。"
/>
<section class="provider-grid">
<article v-for="item in providers" :key="item.id" class="provider-card">
<div class="provider-head">
<div>
<h2>{{ item.name }}</h2>
<p>{{ item.providerCode }}</p>
</div>
<el-tag :type="item.enabled ? 'success' : 'info'">{{ item.enabled ? '已启用' : '未启用' }}</el-tag>
</div>
<dl>
<div><dt>接口地址</dt><dd>{{ item.endpoint }}</dd></div>
<div><dt>访问密钥</dt><dd>{{ item.configured ? '已安全保存' : '未配置' }}</dd></div>
<div>
<dt>连接测试</dt>
<dd>
<el-tag :type="item.lastTestOk ? 'success' : 'warning'">
{{ item.lastTestOk ? '已通过' : '未通过或配置已变更' }}
</el-tag>
<span v-if="item.lastTestTime" class="test-time">{{ formatTestTime(item.lastTestTime) }}</span>
</dd>
</div>
</dl>
<div class="provider-actions">
<el-button @click="openEditor(item)">编辑</el-button>
<el-button :loading="testingId === item.id" @click="testProvider(item)">连接测试</el-button>
<el-button
:type="item.enabled ? 'default' : 'danger'"
:loading="togglingId === item.id"
:disabled="!item.enabled && !item.lastTestOk"
@click="toggleProvider(item)"
>{{ item.enabled ? '停用' : '启用' }}</el-button>
</div>
</article>
<el-empty v-if="!loading && !providers.length" description="尚未配置全网检索提供方" />
</section>
<el-dialog v-model="editorVisible" :title="form.id ? '编辑提供方' : '新增提供方'" width="560px">
<el-form label-position="top">
<el-form-item label="名称"><el-input v-model="form.name" maxlength="100" placeholder="例如:Tavily 生产检索" /></el-form-item>
<el-form-item label="提供方"><el-input model-value="Tavily" disabled /></el-form-item>
<el-form-item label="HTTPS 接口地址"><el-input v-model="form.endpoint" placeholder="https://api.tavily.com/search" /></el-form-item>
<el-form-item :label="form.id ? '访问密钥(留空表示不修改)' : '访问密钥'">
<el-input v-model="form.apiKey" type="password" show-password autocomplete="new-password" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editorVisible = false">取消</el-button>
<el-button type="danger" :loading="saving" @click="submit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="SystemWebSearch" lang="ts">
import { ElMessage } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import {
listWebSearchProviders,
saveWebSearchProvider,
testWebSearchProvider,
toggleWebSearchProvider,
type WebSearchProvider,
type WebSearchProviderForm
} from '@/api/aihr/web-search';
const providers = ref<WebSearchProvider[]>([]);
const loading = ref(false);
const saving = ref(false);
const testingId = ref(0);
const togglingId = ref(0);
const editorVisible = ref(false);
const emptyForm = (): WebSearchProviderForm => ({
name: 'Tavily 全网检索',
providerCode: 'tavily',
endpoint: 'https://api.tavily.com/search',
apiKey: ''
});
const form = reactive<WebSearchProviderForm>(emptyForm());
const load = async () => {
loading.value = true;
try {
providers.value = (await listWebSearchProviders()).data || [];
} catch {
providers.value = [];
ElMessage.error('全网检索配置读取失败');
} finally {
loading.value = false;
}
};
const openEditor = (item?: WebSearchProvider) => {
Object.assign(form, emptyForm(), item ? {
id: item.id,
name: item.name,
providerCode: 'tavily',
endpoint: item.endpoint,
apiKey: ''
} : {});
editorVisible.value = true;
};
const submit = async () => {
if (!form.name.trim() || !form.endpoint.trim() || (!form.id && !form.apiKey.trim())) {
ElMessage.warning('请完整填写名称、接口地址和访问密钥');
return;
}
saving.value = true;
try {
await saveWebSearchProvider({ ...form, name: form.name.trim(), endpoint: form.endpoint.trim() });
editorVisible.value = false;
ElMessage.success('配置已保存,启用前请先执行连接测试');
await load();
} finally {
saving.value = false;
}
};
const testProvider = async (item: WebSearchProvider) => {
testingId.value = item.id;
try {
const result = (await testWebSearchProvider(item.id)).data;
ElMessage({ type: result.ok ? 'success' : 'warning', message: `${result.message}(${result.sourceCount} 个来源)` });
await load();
} finally {
testingId.value = 0;
}
};
const formatTestTime = (value: string) => value.replace('T', ' ').slice(0, 19);
const toggleProvider = async (item: WebSearchProvider) => {
togglingId.value = item.id;
try {
await toggleWebSearchProvider(item.id, !item.enabled);
ElMessage.success(item.enabled ? '已停用全网检索' : '已启用全网检索');
await load();
} finally {
togglingId.value = 0;
}
};
onMounted(load);
</script>
<style scoped>
.web-search-page{padding:24px;color:#172033}.page-head,.provider-head,.provider-actions{display:flex;gap:12px;align-items:center}.page-head,.provider-head{justify-content:space-between}.page-head{margin-bottom:18px}.page-head h1,.provider-head h2{margin:0}.page-head h1{font-size:28px}.page-head p,.provider-head p{margin:7px 0 0;color:#667085}.provider-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:16px;margin-top:18px}.provider-card{padding:20px;border:1px solid #e4e7ec;border-radius:14px;background:#fff;box-shadow:0 4px 18px rgba(16,24,40,.05)}dl{display:grid;gap:12px;margin:20px 0}dl div{display:grid;grid-template-columns:90px minmax(0,1fr);gap:12px}dt{color:#667085}dd{margin:0;overflow-wrap:anywhere}.test-time{margin-left:8px;color:#667085;font-size:12px}.provider-actions{justify-content:flex-end;flex-wrap:wrap}@media(max-width:760px){.web-search-page{padding:16px}.page-head{align-items:flex-start}.provider-grid{grid-template-columns:1fr}.provider-actions{justify-content:flex-start}}
</style>