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:
TradeMate Dev
2026-05-29 11:15:33 +08:00
parent c04fa2c19f
commit 5d2bced39f
31 changed files with 1933 additions and 816 deletions
@@ -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>
+1
View File
@@ -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 模型配置' } },
]
},
]
+210
View File
@@ -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>