feat(web-ai): support qwen web search agent

This commit is contained in:
2026-07-19 21:32:39 +08:00
parent 6853facbe0
commit 7bd2c540fa
7 changed files with 366 additions and 43 deletions
+8 -3
View File
@@ -5,8 +5,10 @@ type ApiResult<T> = { code: number; msg: string; data: T };
export interface WebSearchProvider {
id: number;
name: string;
providerCode: 'tavily' | string;
providerCode: 'tavily' | 'qwen';
endpoint: string;
agentId?: string;
agentVersion?: string;
configured: boolean;
enabled: boolean;
lastTestOk: boolean;
@@ -16,9 +18,11 @@ export interface WebSearchProvider {
export interface WebSearchProviderForm {
id?: number;
name: string;
providerCode: 'tavily';
providerCode: 'tavily' | 'qwen';
endpoint: string;
apiKey: string;
agentId: string;
agentVersion: 'beta' | 'release';
}
export const listWebSearchProviders = (): Promise<ApiResult<WebSearchProvider[]>> => request({
@@ -43,5 +47,6 @@ export const toggleWebSearchProvider = (id: number, enabled: boolean): Promise<A
export const testWebSearchProvider = (id: number): Promise<ApiResult<{ ok: boolean; sourceCount: number; message: string }>> => request({
url: `/api/aihr/web-search/providers/${id}/test`,
method: 'post',
headers: { repeatSubmit: false }
headers: { repeatSubmit: false },
timeout: 45000
});
+54 -14
View File
@@ -21,12 +21,13 @@
<div class="provider-head">
<div>
<h2>{{ item.name }}</h2>
<p>{{ item.providerCode }}</p>
<p>{{ providerLabel(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 v-if="item.providerCode === 'qwen'"><dt>Agent</dt><dd>{{ item.agentId }}({{ item.agentVersion }})</dd></div>
<div><dt>访问密钥</dt><dd>{{ item.configured ? '已安全保存' : '未配置' }}</dd></div>
<div>
<dt>连接测试</dt>
@@ -52,11 +53,25 @@
<el-empty v-if="!loading && !providers.length" description="尚未配置全网检索提供方" />
</section>
<el-dialog v-model="editorVisible" :title="form.id ? '编辑提供方' : '新增提供方'" width="560px">
<el-dialog v-model="editorVisible" class="web-search-editor" :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="提供方">
<el-select v-model="form.providerCode" style="width:100%" @change="changeProvider">
<el-option label="千问联网检索" value="qwen" />
<el-option label="Tavily" value="tavily" />
</el-select>
</el-form-item>
<el-form-item label="HTTPS 接口地址"><el-input v-model="form.endpoint" /></el-form-item>
<template v-if="form.providerCode === 'qwen'">
<el-form-item label="Agent ID"><el-input v-model="form.agentId" placeholder="aid-..." /></el-form-item>
<el-form-item label="Agent Version">
<el-select v-model="form.agentVersion" style="width:100%">
<el-option label="release(已发布)" value="release" />
<el-option label="beta(测试版)" value="beta" />
</el-select>
</el-form-item>
</template>
<el-form-item :label="form.id ? '访问密钥(留空表示不修改)' : '访问密钥'">
<el-input v-model="form.apiKey" type="password" show-password autocomplete="new-password" />
</el-form-item>
@@ -87,11 +102,19 @@ const saving = ref(false);
const testingId = ref(0);
const togglingId = ref(0);
const editorVisible = ref(false);
const providerDefaults = {
qwen: {
name: '千问联网检索',
endpoint: 'https://dashscope.aliyuncs.com/api/v2/apps/web-search-agent/chat/completions'
},
tavily: { name: 'Tavily 全网检索', endpoint: 'https://api.tavily.com/search' }
} as const;
const emptyForm = (): WebSearchProviderForm => ({
name: 'Tavily 全网检索',
providerCode: 'tavily',
endpoint: 'https://api.tavily.com/search',
apiKey: ''
...providerDefaults.qwen,
providerCode: 'qwen',
apiKey: '',
agentId: '',
agentVersion: 'release'
});
const form = reactive<WebSearchProviderForm>(emptyForm());
@@ -111,21 +134,29 @@ const openEditor = (item?: WebSearchProvider) => {
Object.assign(form, emptyForm(), item ? {
id: item.id,
name: item.name,
providerCode: 'tavily',
providerCode: item.providerCode === 'qwen' ? 'qwen' : 'tavily',
endpoint: item.endpoint,
apiKey: ''
apiKey: '',
agentId: item.agentId || '',
agentVersion: item.agentVersion === 'beta' ? 'beta' : 'release'
} : {});
editorVisible.value = true;
};
const submit = async () => {
if (!form.name.trim() || !form.endpoint.trim() || (!form.id && !form.apiKey.trim())) {
ElMessage.warning('请完整填写名称、接口地址和访问密钥');
const qwenIncomplete = form.providerCode === 'qwen' && (!form.agentId.trim() || !form.agentVersion);
if (!form.name.trim() || !form.endpoint.trim() || (!form.id && !form.apiKey.trim()) || qwenIncomplete) {
ElMessage.warning('请完整填写名称、接口地址、访问密钥和 Agent 信息');
return;
}
saving.value = true;
try {
await saveWebSearchProvider({ ...form, name: form.name.trim(), endpoint: form.endpoint.trim() });
await saveWebSearchProvider({
...form,
name: form.name.trim(),
endpoint: form.endpoint.trim(),
agentId: form.agentId.trim()
});
editorVisible.value = false;
ElMessage.success('配置已保存,启用前请先执行连接测试');
await load();
@@ -134,6 +165,15 @@ const submit = async () => {
}
};
const changeProvider = (code: 'tavily' | 'qwen') => {
Object.assign(form, providerDefaults[code], {
agentId: code === 'qwen' ? form.agentId : '',
agentVersion: code === 'qwen' ? form.agentVersion : 'release'
});
};
const providerLabel = (code: string) => code === 'qwen' ? '千问联网检索' : 'Tavily';
const testProvider = async (item: WebSearchProvider) => {
testingId.value = item.id;
try {
@@ -162,5 +202,5 @@ 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}}
.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}:deep(.web-search-editor .el-dialog__body){max-height:calc(100vh - 180px);overflow-y:auto}@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>