feat: restore learning feedback and knowledge capabilities
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user