docs: update project docs and clean up redundant files
- PROGRESS.md: update to 2026-05-29 with security hardening (T-005), 4-frontend architecture, AI provider refactoring, discovery features, landing page/referral/quota, desktop layout, admin AI management - AGENTS.md: add AI provider list (Alibaba/NVIDIA, removed Claude/DeepL/Local), DB-driven config, CSRF/rate-limit/CORS notes, admin_ai reload quirk - .env.example: sync with actual config, replace deprecated providers with current Sensenova/OpencodeGo/NVIDIA/Spark/Alibaba - docs/PROJECT_STATUS.md: archive (fully superseded by PROGRESS.md) - Remove generated JS files (_bing_search.js, _batch_search.js) - Remove empty directories (data/corpus, data/models) - Remove backend/.coverage (test artifact) - Fix services/.gitignore to cover _bing_search.js - Include pending AI provider DB admin feature (admin_ai, AIProvider model, AIProviders.vue, migration) and T-008 test report
This commit is contained in:
@@ -51,6 +51,10 @@
|
||||
<el-icon><Search /></el-icon>
|
||||
<span>搜索配置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/system/ai-providers">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span>AI 模型配置</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
@@ -74,6 +74,7 @@ const routes = [
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'SearchConfig', component: () => import('@/views/SearchConfig.vue'), meta: { title: '搜索配置' } },
|
||||
{ path: 'ai-providers', name: 'AIProviders', component: () => import('@/views/AIProviders.vue'), meta: { title: 'AI 模型配置' } },
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<h3 style="margin:0">AI 模型配置</h3>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-button @click="reloadFromDB">重载配置</el-button>
|
||||
<el-button type="primary" @click="showAdd">添加模型</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="statusInfo" :title="statusInfo" type="info" show-icon :closable="true" style="margin-bottom:16px" />
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="name" label="名称" width="150" />
|
||||
<el-table-column prop="type_label" label="类型" width="160" />
|
||||
<el-table-column prop="model_name" label="模型" width="160">
|
||||
<template #default="{ row }">
|
||||
<code style="font-size:12px">{{ row.model_name }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="base_url" label="接口地址" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.base_url" style="font-size:12px;color:#999">{{ row.base_url }}</span>
|
||||
<span v-else style="color:#ccc">默认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="api_key" label="API Key" width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.api_key" style="font-family:monospace;font-size:12px">{{ row.api_key }}</span>
|
||||
<span v-else style="color:#999">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="80" align="center" />
|
||||
<el-table-column prop="enabled" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editProvider(row)">编辑</el-button>
|
||||
<el-popconfirm title="确认删除?" @confirm="deleteProvider(row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger" plain>删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.isEdit ? '编辑 AI 模型' : '添加 AI 模型'" width="580px">
|
||||
<el-form :model="form" label-width="110px" label-position="top">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="form.name" placeholder="例:商汤 DeepSeek" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提供商类型" required>
|
||||
<el-select v-model="form.provider_type" style="width:100%">
|
||||
<el-option value="sensenova" label="Sensenova (商汤)" />
|
||||
<el-option value="opencode_go" label="OpencodeGo" />
|
||||
<el-option value="nvidia" label="NVIDIA" />
|
||||
<el-option value="spark" label="讯飞 Spark" />
|
||||
<el-option value="alibaba-mt" label="阿里翻译 (Alibaba MT)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称" required>
|
||||
<el-input v-model="form.model_name" placeholder="例:deepseek-v4-flash" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Key" required>
|
||||
<el-input v-model="form.api_key" placeholder="API Key" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.provider_type === 'alibaba-mt'" label="API Secret" required>
|
||||
<el-input v-model="form.api_secret" placeholder="Access Key Secret" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="接口地址">
|
||||
<el-input v-model="form.base_url" placeholder="留空则使用提供商默认地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级(越小越优先)">
|
||||
<el-input-number v-model="form.priority" :min="0" :max="99" />
|
||||
<span style="font-size:12px;color:#999;margin-left:8px">0=最高</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveProvider">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import http from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const list = ref([])
|
||||
const statusInfo = ref('')
|
||||
|
||||
const dialog = reactive({ visible: false, isEdit: false, id: null })
|
||||
const form = reactive({
|
||||
name: '',
|
||||
provider_type: 'sensenova',
|
||||
api_key: '',
|
||||
api_secret: '',
|
||||
base_url: '',
|
||||
model_name: 'deepseek-v4-flash',
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await http.get('/admin/ai-providers')
|
||||
list.value = res.items || []
|
||||
} catch (e) { ElMessage.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const res = await http.get('/admin/ai-providers/status')
|
||||
if (res.active_providers?.length) {
|
||||
statusInfo.value = `当前活跃提供商: ${res.active_providers.join(', ')} | 共 ${res.provider_count} 个`
|
||||
} else {
|
||||
statusInfo.value = '暂无活跃的 AI 提供商,请添加并启用'
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function showAdd() {
|
||||
dialog.isEdit = false
|
||||
dialog.id = null
|
||||
form.name = ''
|
||||
form.provider_type = 'sensenova'
|
||||
form.api_key = ''
|
||||
form.api_secret = ''
|
||||
form.base_url = ''
|
||||
form.model_name = 'deepseek-v4-flash'
|
||||
form.priority = 0
|
||||
form.enabled = true
|
||||
dialog.visible = true
|
||||
}
|
||||
|
||||
function editProvider(p) {
|
||||
dialog.isEdit = true
|
||||
dialog.id = p.id
|
||||
form.name = p.name
|
||||
form.provider_type = p.provider_type
|
||||
form.api_key = p.api_key || ''
|
||||
form.api_secret = ''
|
||||
form.base_url = p.base_url || ''
|
||||
form.model_name = p.model_name
|
||||
form.priority = p.priority
|
||||
form.enabled = p.enabled
|
||||
dialog.visible = true
|
||||
}
|
||||
|
||||
async function saveProvider() {
|
||||
if (!form.name || !form.provider_type || !form.model_name) { ElMessage.warning('请填写名称、类型和模型'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: form.name,
|
||||
provider_type: form.provider_type,
|
||||
api_key: form.api_key || null,
|
||||
api_secret: form.api_secret || null,
|
||||
base_url: form.base_url || null,
|
||||
model_name: form.model_name,
|
||||
extra_config: {},
|
||||
priority: form.priority,
|
||||
enabled: form.enabled,
|
||||
}
|
||||
if (dialog.isEdit) {
|
||||
await http.put(`/admin/ai-providers/${dialog.id}`, data)
|
||||
ElMessage.success('已更新,AI 路由器已重载')
|
||||
} else {
|
||||
await http.post('/admin/ai-providers', data)
|
||||
ElMessage.success('已添加')
|
||||
}
|
||||
dialog.visible = false
|
||||
await fetchList()
|
||||
await fetchStatus()
|
||||
} catch (e) { ElMessage.error('保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function deleteProvider(p) {
|
||||
try {
|
||||
await http.delete(`/admin/ai-providers/${p.id}`)
|
||||
ElMessage.success('已删除')
|
||||
await fetchList()
|
||||
await fetchStatus()
|
||||
} catch (e) { ElMessage.error('删除失败') }
|
||||
}
|
||||
|
||||
async function reloadFromDB() {
|
||||
try {
|
||||
const res = await http.post('/admin/ai-providers/reload')
|
||||
ElMessage.success(res.message)
|
||||
await fetchStatus()
|
||||
} catch (e) { ElMessage.error('重载失败') }
|
||||
}
|
||||
|
||||
onMounted(() => { fetchList(); fetchStatus() })
|
||||
</script>
|
||||
Reference in New Issue
Block a user