20 changed files with 1311 additions and 89 deletions
@ -0,0 +1,172 @@ |
|||||
|
# Enterprise AI Platform - 完善计划 v2(已实施) |
||||
|
|
||||
|
> 基于全面代码审查(backend 14个模块 + frontend 28个页面) |
||||
|
> 日期: 2026-05-11 |
||||
|
> 状态: **全部已实施** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 一、前后端功能对齐矩阵 |
||||
|
|
||||
|
### 1.1 已对齐(✅ 全部模块前后端完整) |
||||
|
|
||||
|
| 模块 | 后端 API | 前端页面 | 对齐度 | |
||||
|
|------|---------|---------|--------| |
||||
|
| Auth 认证 | login/me/put-me/put-password/wecom-oauth | Login.vue / Profile.vue | ✅ | |
||||
|
| Org 组织架构 | 16个端点 CRUD | DepartmentTree.vue / UserList.vue | ✅ | |
||||
|
| RBAC 角色权限 | 8个端点 CRUD | RoleList.vue / PermissionConfig.vue | ✅ | |
||||
|
| Task 任务管理 | 8个端点 CRUD+push+delete | TaskList.vue / TaskCreate.vue / TaskDetail.vue | ✅ | |
||||
|
| Flow 流编排 | 10个端点 CRUD+test+publish | FlowList.vue / FlowEditor.vue / FlowMarket.vue / FlowDetail.vue | ✅ | |
||||
|
| Agent 智能体 | 4个端点 list/chat/history | AgentList.vue / AgentChat.vue | ✅ | |
||||
|
| MCP 注册中心 | 7个端点 CRUD+test | McpManager.vue | ✅ | |
||||
|
| Document 文档 | 5个端点 upload/parse/delete | DocumentManager.vue | ✅ | |
||||
|
| Monitor 监控 | 5个端点 list/dashboard/analysis | EmployeeList.vue / WorkDashboard.vue / AIAnalysis.vue | ✅ | |
||||
|
| Notification 通知 | 8个端点 template/send/websocket | NotificationCenter.vue | ✅ | |
||||
|
| Audit 审计 | 4个端点 list/export/stats | AuditLog.vue | ✅ | |
||||
|
| System 系统 | 4个端点 health/stats/metrics | SystemMonitor.vue + Dashboard.vue | ✅ | |
||||
|
| WeCom 企微 | 7个端点 callback/send/config/update | BotConfig.vue (含消息发送) | ✅ | |
||||
|
| RAG 知识库 | 4个端点 upload/index/search | KnowledgeBase.vue | ✅ | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 二、页面功能完成度详细评估 |
||||
|
|
||||
|
### 2.1 用户端页面(14个) |
||||
|
|
||||
|
| 页面 | 文件 | 状态 | |
||||
|
|------|------|------| |
||||
|
| 工作台 | Dashboard.vue | ✅ 完整 | |
||||
|
| 员工监控 | EmployeeList.vue | ✅ 完整 | |
||||
|
| 工作看板 | WorkDashboard.vue | ✅ 完整 | |
||||
|
| AI分析 | AIAnalysis.vue | ✅ 完整 | |
||||
|
| 任务列表 | TaskList.vue | ✅ 完整 (含编辑/删除) | |
||||
|
| 任务详情 | TaskDetail.vue | ✅ 完整 | |
||||
|
| 智能体列表 | AgentList.vue | ✅ 完整 | |
||||
|
| 智能体对话 | AgentChat.vue | ✅ 完整 (4种Agent差异化) | |
||||
|
| 文档管理 | DocumentManager.vue | ✅ 完整 | |
||||
|
| 知识库 | KnowledgeBase.vue | ✅ 完整 (**新增**) | |
||||
|
| 企微配置 | BotConfig.vue | ✅ 完整 (**已修复保存+消息发送**) | |
||||
|
| 通知中心 | NotificationCenter.vue | ✅ 完整 | |
||||
|
| 个人中心 | Profile.vue | ✅ 完整 (**新增**) | |
||||
|
| 系统配置 | Settings.vue | ✅ 完整 | |
||||
|
|
||||
|
### 2.2 管理端页面(14个) |
||||
|
|
||||
|
| 页面 | 文件 | 状态 | |
||||
|
|------|------|------| |
||||
|
| 控制台 | Dashboard.vue (admin) | ✅ 完整 | |
||||
|
| 部门管理 | DepartmentTree.vue | ✅ 完整 | |
||||
|
| 人员管理 | UserList.vue | ✅ 完整 | |
||||
|
| 角色管理 | RoleList.vue | ✅ 完整 | |
||||
|
| 权限配置 | PermissionConfig.vue | ✅ 完整 | |
||||
|
| 流列表 | FlowList.vue | ✅ 完整 | |
||||
|
| 流编辑器 | FlowEditor.vue | ✅ 完整 (vue-flow拖拽) | |
||||
|
| 流市场 | FlowMarket.vue | ✅ 完整 (**已修复详情入口**) | |
||||
|
| 流详情 | FlowDetail.vue | ✅ 完整 (**新增**) | |
||||
|
| MCP服务管理 | McpManager.vue | ✅ 完整 (**新增**) | |
||||
|
| 知识库管理 | KnowledgeBase.vue | ✅ 完整 (**新增**) | |
||||
|
| 创建任务 | TaskCreate.vue | ✅ 完整 | |
||||
|
| 审计日志 | AuditLog.vue | ✅ 完整 | |
||||
|
| 系统监控 | SystemMonitor.vue | ✅ 完整 | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 三、已实施修复清单 |
||||
|
|
||||
|
### 3.1 新增页面(6个)✅ |
||||
|
|
||||
|
| # | 页面 | 文件 | 路由 | |
||||
|
|---|------|------|------| |
||||
|
| 1 | MCP服务管理 | `views/mcp/McpManager.vue` | `/admin/mcp/manager` | |
||||
|
| 2 | RAG知识库管理 | `views/rag/KnowledgeBase.vue` | `/user/rag/knowledge` + `/admin/rag/knowledge` | |
||||
|
| 3 | 流市场详情 | `views/flow/FlowDetail.vue` | `/admin/flow/market/:id` | |
||||
|
| 4 | 个人中心 | `views/profile/Profile.vue` | `/user/profile` | |
||||
|
|
||||
|
### 3.2 功能修复(8项)✅ |
||||
|
|
||||
|
| # | 修复 | 文件 | 说明 | |
||||
|
|---|------|------|------| |
||||
|
| 5 | 企微配置保存 | BotConfig.vue | 调用 `wecomApi.updateConfig()` 真实保存到 .env | |
||||
|
| 6 | 企微消息发送 | BotConfig.vue | 新增消息发送面板,调用 `wecomApi.sendMessage()` | |
||||
|
| 7 | 流市场详情入口 | FlowMarket.vue | 点击卡片跳转到 FlowDetail,支持安装 | |
||||
|
| 8 | 任务编辑/删除 | TaskList.vue | 新增编辑/删除按钮+确认弹窗 | |
||||
|
| 9 | 任务删除API | backend task/router.py | 新增 `DELETE /tasks/:id` 端点 | |
||||
|
| 10 | 个人信息更新API | backend auth/router.py | 新增 `PUT /auth/me` + `PUT /auth/password` | |
||||
|
| 11 | 企微配置保存API | backend wecom/router.py | 新增 `PUT /wecom/config` + `POST /wecom/send` | |
||||
|
| 12 | API层补全 | frontend api/index.ts | 新增 deleteTask/updateConfig/sendMessage 等 | |
||||
|
|
||||
|
### 3.3 路由/侧边栏注册 ✅ |
||||
|
|
||||
|
| 位置 | 新增项 | |
||||
|
|------|--------| |
||||
|
| router/index.ts | KnowledgeBase / Profile / FlowDetail / McpManager / AdminKnowledgeBase / AdminMcpManager | |
||||
|
| MainLayout.vue | 知识库 / 个人中心 菜单项 | |
||||
|
| AdminLayout.vue | MCP服务 / 知识库管理 菜单项 | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 四、账号权限测试矩阵 |
||||
|
|
||||
|
### 4.1 测试账号 |
||||
|
|
||||
|
| 账号 | 密码 | 角色 | 权限 | 用途 | |
||||
|
|------|------|------|------|------| |
||||
|
| **sroot** | admin123 | root | `*:*` (无条件全部) | 超级管理员,测试所有功能 | |
||||
|
| **admin** | admin123 | super_admin | 全部功能权限 | 系统管理员 | |
||||
|
| **manager** | manager123 | dept_manager | 下属数据+管理权限 | 部门经理测试 | |
||||
|
| **employee** | employee123 | employee | 仅自己数据 | 普通员工测试 | |
||||
|
|
||||
|
### 4.2 各角色可见页面 |
||||
|
|
||||
|
| 页面 | sroot | admin | manager | employee | |
||||
|
|------|:-----:|:-----:|:-------:|:--------:| |
||||
|
| 工作台/监控/任务/智能体/文档 | ✅ | ✅ | ✅ | ✅ | |
||||
|
| 知识库/企微配置/通知中心/个人中心 | ✅ | ✅ | ✅ | ✅ | |
||||
|
| 管理后台入口 | ✅ | ✅ | ❌ | ❌ | |
||||
|
| 部门管理/人员管理 | ✅ | ✅ | ⚠️ 只读 | ❌ | |
||||
|
| 角色管理/权限配置 | ✅ | ✅ | ❌ | ❌ | |
||||
|
| 流编排/流市场/流详情 | ✅ | ✅ | ✅ | ⚠️ 只读 | |
||||
|
| MCP服务管理/知识库管理 | ✅ | ✅ | ❌ | ❌ | |
||||
|
| 创建任务/编辑/删除 | ✅ | ✅ | ✅ | ❌ | |
||||
|
| 审计日志 | ✅ | ✅ | ❌ | ❌ | |
||||
|
| 系统监控 | ✅ | ✅ | ❌ | ❌ | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 五、测试检查清单 |
||||
|
|
||||
|
使用 **sroot** 账号登录后,按以下顺序验证: |
||||
|
|
||||
|
``` |
||||
|
□ 登录成功,token 持久化 |
||||
|
□ 刷新页面后权限不丢失,所有菜单正常显示 |
||||
|
□ 工作台数据正常加载 |
||||
|
□ 员工监控列表+工作看板+AI分析 可正常访问 |
||||
|
□ 任务列表+创建+详情+编辑+删除 完整流程 |
||||
|
□ 智能体4种类型对话正常,UI有区分 |
||||
|
□ 文档上传+解析+格式化+删除 |
||||
|
□ 知识库上传+检索+手动索引 |
||||
|
□ 企微配置可保存+可发送消息 |
||||
|
□ 通知中心可接收消息 |
||||
|
□ 个人中心修改信息+修改密码 |
||||
|
□ 系统配置 LLM/企微/RAG 可修改 |
||||
|
□ 管理后台入口可见 |
||||
|
□ 部门/人员/角色 CRUD |
||||
|
□ 流编辑器拖拽+保存+验证+发布 |
||||
|
□ 流市场列表+点击查看详情+安装 |
||||
|
□ MCP服务注册+编辑+测试+删除 |
||||
|
□ 审计日志列表+导出 |
||||
|
□ 系统监控健康检查 |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 六、剩余可优化项(非必要) |
||||
|
|
||||
|
| # | 项目 | 说明 | 优先级 | |
||||
|
|---|------|------|--------| |
||||
|
| 1 | 全局搜索 | 顶部导航栏全局搜索框 | 🟢 P3 | |
||||
|
| 2 | 操作引导 | 首次登录的新手引导 | 🟢 P3 | |
||||
|
| 3 | 主题切换 | 深色/浅色模式切换 | 🟢 P3 | |
||||
|
| 4 | 国际化 | i18n 多语言支持 | 🟢 P3 | |
||||
|
| 5 | 移动端适配 | 响应式布局优化 | 🟢 P3 | |
||||
@ -1,2 +1,7 @@ |
|||||
# hg-agents |
# hg-agents |
||||
|
|
||||
|
sroot admin123 root ( *:* ) 无条件测试所有功能 |
||||
|
admin admin123 super_admin 系统管理员测试 |
||||
|
manager manager123 dept_manager 部门经理权限测试 |
||||
|
employee employee123 employee 普通员工权限测试 |
||||
|
|
||||
|
|||||
@ -0,0 +1,104 @@ |
|||||
|
<template> |
||||
|
<div class="flow-detail-page"> |
||||
|
<el-page-header @back="$router.back()" :content="flow?.name || '流详情'" /> |
||||
|
|
||||
|
<el-row :gutter="20" style="margin-top: 20px"> |
||||
|
<el-col :span="16"> |
||||
|
<el-card> |
||||
|
<template #header>流定义</template> |
||||
|
<el-descriptions :column="2" border> |
||||
|
<el-descriptions-item label="名称">{{ flow?.name }}</el-descriptions-item> |
||||
|
<el-descriptions-item label="状态"> |
||||
|
<el-tag :type="flow?.status === 'published' ? 'success' : 'info'">{{ flow?.status }}</el-tag> |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="创建者">{{ flow?.created_by || '-' }}</el-descriptions-item> |
||||
|
<el-descriptions-item label="创建时间">{{ flow?.created_at ? new Date(flow.created_at).toLocaleString() : '-' }}</el-descriptions-item> |
||||
|
<el-descriptions-item label="描述" :span="2">{{ flow?.description || '无' }}</el-descriptions-item> |
||||
|
</el-descriptions> |
||||
|
|
||||
|
<el-divider>节点列表</el-divider> |
||||
|
<el-table :data="nodes" stripe size="small"> |
||||
|
<el-table-column prop="id" label="ID" width="120" /> |
||||
|
<el-table-column prop="type" label="类型" width="120"> |
||||
|
<template #default="{ row }"><el-tag size="small">{{ row.type }}</el-tag></template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="label" label="名称" min-width="160" /> |
||||
|
</el-table> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :span="8"> |
||||
|
<el-card> |
||||
|
<template #header>操作</template> |
||||
|
<el-button type="primary" style="width: 100%; margin-bottom: 12px" @click="installFlow" :loading="installing"> |
||||
|
安装到我的工作流 |
||||
|
</el-button> |
||||
|
<el-button style="width: 100%; margin-bottom: 12px" @click="testRun" :loading="testing"> |
||||
|
测试运行 |
||||
|
</el-button> |
||||
|
<el-button style="width: 100%" @click="$router.push(`/admin/flow/editor/${flowId}`)"> |
||||
|
编辑此流 |
||||
|
</el-button> |
||||
|
</el-card> |
||||
|
|
||||
|
<el-card style="margin-top: 20px"> |
||||
|
<template #header>连接关系</template> |
||||
|
<div v-for="(edge, i) in edges" :key="i" style="padding: 4px 0; border-bottom: 1px solid #f0f0f0; font-size: 13px"> |
||||
|
{{ edge.source }} → {{ edge.target }} |
||||
|
</div> |
||||
|
<el-empty v-if="!edges.length" description="无连接" :image-size="40" /> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, onMounted } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { ElMessage } from 'element-plus' |
||||
|
import { flowApi } from '@/api' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const flowId = computed(() => route.params.id as string) |
||||
|
const flow = ref<any>(null) |
||||
|
const nodes = ref<any[]>([]) |
||||
|
const edges = ref<any[]>([]) |
||||
|
const installing = ref(false) |
||||
|
const testing = ref(false) |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
const res: any = await flowApi.getFlow(flowId.value) |
||||
|
flow.value = res?.data || res || {} |
||||
|
const def = flow.value.definition_json || {} |
||||
|
nodes.value = def.nodes || [] |
||||
|
edges.value = def.edges || [] |
||||
|
}) |
||||
|
|
||||
|
async function installFlow() { |
||||
|
installing.value = true |
||||
|
try { |
||||
|
const res: any = await flowApi.createFlow({ |
||||
|
name: flow.value.name + ' (副本)', |
||||
|
description: flow.value.description, |
||||
|
nodes: nodes.value, |
||||
|
edges: edges.value, |
||||
|
trigger: {}, |
||||
|
}) |
||||
|
const newFlow = res?.data || res || {} |
||||
|
ElMessage.success('已安装到我的工作流') |
||||
|
router.push(`/admin/flow/editor/${newFlow.id}`) |
||||
|
} catch { ElMessage.error('安装失败') } |
||||
|
finally { installing.value = false } |
||||
|
} |
||||
|
|
||||
|
async function testRun() { |
||||
|
testing.value = true |
||||
|
try { |
||||
|
await flowApi.testFlow(flowId.value) |
||||
|
ElMessage.success('测试运行完成') |
||||
|
} catch { ElMessage.error('测试失败') } |
||||
|
finally { testing.value = false } |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,179 @@ |
|||||
|
<template> |
||||
|
<div class="mcp-page"> |
||||
|
<el-card> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>MCP 服务管理</span> |
||||
|
<el-button type="primary" @click="showAddDialog">注册新服务</el-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<el-table :data="servers" v-loading="loading" stripe> |
||||
|
<el-table-column prop="name" label="服务名称" min-width="140" /> |
||||
|
<el-table-column prop="description" label="描述" min-width="200" /> |
||||
|
<el-table-column prop="server_type" label="类型" width="100"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-tag size="small">{{ row.server_type || 'http' }}</el-tag> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="base_url" label="地址" min-width="200" /> |
||||
|
<el-table-column prop="status" label="状态" width="90"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small"> |
||||
|
{{ row.status === 'active' ? '启用' : '停用' }} |
||||
|
</el-tag> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="操作" width="220" fixed="right"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-button size="small" @click="testServer(row)" :loading="row._testing">测试</el-button> |
||||
|
<el-button size="small" type="primary" @click="editServer(row)">编辑</el-button> |
||||
|
<el-button size="small" type="danger" @click="deleteServer(row)">删除</el-button> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</el-card> |
||||
|
|
||||
|
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑 MCP 服务' : '注册 MCP 服务'" width="600px"> |
||||
|
<el-form :model="form" label-width="100px"> |
||||
|
<el-form-item label="服务名称" required> |
||||
|
<el-input v-model="form.name" placeholder="如: weather-service" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="描述"> |
||||
|
<el-input v-model="form.description" type="textarea" :rows="2" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="类型"> |
||||
|
<el-select v-model="form.server_type" style="width: 100%"> |
||||
|
<el-option label="HTTP" value="http" /> |
||||
|
<el-option label="SSE" value="sse" /> |
||||
|
<el-option label="Stdio" value="stdio" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="服务地址" required> |
||||
|
<el-input v-model="form.base_url" placeholder="http://host:port/mcp" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="API Key"> |
||||
|
<el-input v-model="form.api_key" type="password" show-password placeholder="可选" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="状态"> |
||||
|
<el-switch v-model="form.status_active" active-text="启用" inactive-text="停用" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="工具列表"> |
||||
|
<el-input v-model="form.tools_str" type="textarea" :rows="3" placeholder="每行一个工具名,如: search get_weather" /> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
<template #footer> |
||||
|
<el-button @click="dialogVisible = false">取消</el-button> |
||||
|
<el-button type="primary" @click="saveServer" :loading="saving">保存</el-button> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, reactive, onMounted } from 'vue' |
||||
|
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
|
import { mcpApi } from '@/api' |
||||
|
|
||||
|
const loading = ref(false) |
||||
|
const saving = ref(false) |
||||
|
const servers = ref<any[]>([]) |
||||
|
const dialogVisible = ref(false) |
||||
|
const isEdit = ref(false) |
||||
|
const editId = ref('') |
||||
|
|
||||
|
const form = reactive({ |
||||
|
name: '', |
||||
|
description: '', |
||||
|
server_type: 'http', |
||||
|
base_url: '', |
||||
|
api_key: '', |
||||
|
status_active: true, |
||||
|
tools_str: '', |
||||
|
}) |
||||
|
|
||||
|
async function loadServers() { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const res: any = await mcpApi.getServers() |
||||
|
const data = res?.data || res || [] |
||||
|
servers.value = Array.isArray(data) ? data.map((s: any) => ({ ...s, _testing: false })) : [] |
||||
|
} finally { loading.value = false } |
||||
|
} |
||||
|
|
||||
|
function showAddDialog() { |
||||
|
isEdit.value = false |
||||
|
editId.value = '' |
||||
|
Object.assign(form, { name: '', description: '', server_type: 'http', base_url: '', api_key: '', status_active: true, tools_str: '' }) |
||||
|
dialogVisible.value = true |
||||
|
} |
||||
|
|
||||
|
function editServer(row: any) { |
||||
|
isEdit.value = true |
||||
|
editId.value = row.id |
||||
|
Object.assign(form, { |
||||
|
name: row.name, |
||||
|
description: row.description || '', |
||||
|
server_type: row.server_type || 'http', |
||||
|
base_url: row.base_url || '', |
||||
|
api_key: row.api_key || '', |
||||
|
status_active: row.status === 'active', |
||||
|
tools_str: (row.tools || []).join('\n'), |
||||
|
}) |
||||
|
dialogVisible.value = true |
||||
|
} |
||||
|
|
||||
|
async function saveServer() { |
||||
|
if (!form.name || !form.base_url) { ElMessage.warning('请填写服务名称和地址'); return } |
||||
|
saving.value = true |
||||
|
try { |
||||
|
const payload = { |
||||
|
name: form.name, |
||||
|
description: form.description, |
||||
|
server_type: form.server_type, |
||||
|
base_url: form.base_url, |
||||
|
api_key: form.api_key || undefined, |
||||
|
status: form.status_active ? 'active' : 'inactive', |
||||
|
tools: form.tools_str.split('\n').map(s => s.trim()).filter(Boolean), |
||||
|
} |
||||
|
if (isEdit.value) { |
||||
|
await mcpApi.updateServer(editId.value, payload) |
||||
|
ElMessage.success('更新成功') |
||||
|
} else { |
||||
|
await mcpApi.createServer(payload) |
||||
|
ElMessage.success('注册成功') |
||||
|
} |
||||
|
dialogVisible.value = false |
||||
|
await loadServers() |
||||
|
} finally { saving.value = false } |
||||
|
} |
||||
|
|
||||
|
async function testServer(row: any) { |
||||
|
row._testing = true |
||||
|
try { |
||||
|
const res: any = await mcpApi.testServer(row.id) |
||||
|
const data = res?.data || res || {} |
||||
|
if (data.success) { |
||||
|
ElMessage.success(`测试成功: ${data.tools_count || 0} 个工具可用`) |
||||
|
} else { |
||||
|
ElMessage.warning(`测试失败: ${data.error || '连接异常'}`) |
||||
|
} |
||||
|
} catch { ElMessage.error('测试失败') } |
||||
|
finally { row._testing = false } |
||||
|
} |
||||
|
|
||||
|
async function deleteServer(row: any) { |
||||
|
try { |
||||
|
await ElMessageBox.confirm(`确认删除 MCP 服务 "${row.name}"?`, '提示', { type: 'warning' }) |
||||
|
await mcpApi.deleteServer(row.id) |
||||
|
ElMessage.success('已删除') |
||||
|
await loadServers() |
||||
|
} catch { /**/ } |
||||
|
} |
||||
|
|
||||
|
onMounted(loadServers) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; } |
||||
|
</style> |
||||
@ -0,0 +1,126 @@ |
|||||
|
<template> |
||||
|
<div class="profile-page"> |
||||
|
<el-row :gutter="20"> |
||||
|
<el-col :span="14"> |
||||
|
<el-card> |
||||
|
<template #header>个人信息</template> |
||||
|
<el-form :model="profileForm" label-width="100px"> |
||||
|
<el-form-item label="用户名"> |
||||
|
<el-input :model-value="userStore.username" disabled /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="显示名称"> |
||||
|
<el-input v-model="profileForm.display_name" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="邮箱"> |
||||
|
<el-input v-model="profileForm.email" placeholder="your@email.com" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="手机"> |
||||
|
<el-input v-model="profileForm.phone" placeholder="手机号" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="部门"> |
||||
|
<el-input :model-value="userStore.user?.department_id || '-'" disabled /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="角色"> |
||||
|
<el-tag v-for="r in userStore.roleCodes" :key="r" style="margin-right: 6px">{{ r }}</el-tag> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="saveProfile" :loading="saving">保存</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :span="10"> |
||||
|
<el-card> |
||||
|
<template #header>修改密码</template> |
||||
|
<el-form :model="passwordForm" label-width="100px"> |
||||
|
<el-form-item label="当前密码"> |
||||
|
<el-input v-model="passwordForm.old_password" type="password" show-password /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="新密码"> |
||||
|
<el-input v-model="passwordForm.new_password" type="password" show-password /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="确认密码"> |
||||
|
<el-input v-model="passwordForm.confirm_password" type="password" show-password /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="changePassword" :loading="changing">修改密码</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</el-card> |
||||
|
|
||||
|
<el-card style="margin-top: 20px"> |
||||
|
<template #header>登录信息</template> |
||||
|
<el-descriptions :column="1" border size="small"> |
||||
|
<el-descriptions-item label="登录时间">{{ new Date().toLocaleString() }}</el-descriptions-item> |
||||
|
<el-descriptions-item label="权限数">{{ userStore.permissions.length }}</el-descriptions-item> |
||||
|
<el-descriptions-item label="超级管理员">{{ userStore.isSuperAdmin ? '是' : '否' }}</el-descriptions-item> |
||||
|
</el-descriptions> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, reactive, onMounted } from 'vue' |
||||
|
import { ElMessage } from 'element-plus' |
||||
|
import { useUserStore } from '@/stores/user' |
||||
|
import api from '@/api' |
||||
|
|
||||
|
const userStore = useUserStore() |
||||
|
const saving = ref(false) |
||||
|
const changing = ref(false) |
||||
|
|
||||
|
const profileForm = reactive({ |
||||
|
display_name: '', |
||||
|
email: '', |
||||
|
phone: '', |
||||
|
}) |
||||
|
|
||||
|
const passwordForm = reactive({ |
||||
|
old_password: '', |
||||
|
new_password: '', |
||||
|
confirm_password: '', |
||||
|
}) |
||||
|
|
||||
|
onMounted(() => { |
||||
|
const u = userStore.user || {} |
||||
|
profileForm.display_name = u.display_name || '' |
||||
|
profileForm.email = u.email || '' |
||||
|
profileForm.phone = u.phone || '' |
||||
|
}) |
||||
|
|
||||
|
async function saveProfile() { |
||||
|
saving.value = true |
||||
|
try { |
||||
|
await api.put('/auth/me', profileForm) |
||||
|
ElMessage.success('个人信息已更新') |
||||
|
await userStore.fetchUser() |
||||
|
} catch { ElMessage.error('更新失败') } |
||||
|
finally { saving.value = false } |
||||
|
} |
||||
|
|
||||
|
async function changePassword() { |
||||
|
if (!passwordForm.old_password || !passwordForm.new_password) { |
||||
|
ElMessage.warning('请填写密码') |
||||
|
return |
||||
|
} |
||||
|
if (passwordForm.new_password !== passwordForm.confirm_password) { |
||||
|
ElMessage.warning('两次密码不一致') |
||||
|
return |
||||
|
} |
||||
|
changing.value = true |
||||
|
try { |
||||
|
await api.put('/auth/password', { |
||||
|
old_password: passwordForm.old_password, |
||||
|
new_password: passwordForm.new_password, |
||||
|
}) |
||||
|
ElMessage.success('密码已修改') |
||||
|
passwordForm.old_password = '' |
||||
|
passwordForm.new_password = '' |
||||
|
passwordForm.confirm_password = '' |
||||
|
} catch { ElMessage.error('修改失败') } |
||||
|
finally { changing.value = false } |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,140 @@ |
|||||
|
<template> |
||||
|
<div class="knowledge-page"> |
||||
|
<el-row :gutter="20"> |
||||
|
<el-col :span="16"> |
||||
|
<el-card> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>知识库文档</span> |
||||
|
<el-upload :http-request="uploadDoc" :show-file-list="false" accept=".pdf,.docx,.doc,.xlsx,.xls,.txt,.md"> |
||||
|
<el-button type="primary">上传文档</el-button> |
||||
|
</el-upload> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<el-table :data="documents" v-loading="loading" stripe> |
||||
|
<el-table-column prop="file_name" label="文件名" min-width="200" /> |
||||
|
<el-table-column prop="file_type" label="类型" width="80"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-tag size="small">{{ row.file_type || 'txt' }}</el-tag> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="chunk_count" label="分块数" width="80" /> |
||||
|
<el-table-column prop="indexed_at" label="索引时间" width="170"> |
||||
|
<template #default="{ row }">{{ row.indexed_at ? new Date(row.indexed_at).toLocaleString() : '-' }}</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="操作" width="120"> |
||||
|
<template #default="{ row }"> |
||||
|
<el-button size="small" type="danger" @click="deleteDoc(row)">删除</el-button> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :span="8"> |
||||
|
<el-card> |
||||
|
<template #header>检索测试</template> |
||||
|
<el-input v-model="query" placeholder="输入问题检索知识库" @keyup.enter="doSearch" style="margin-bottom: 12px" /> |
||||
|
<el-button type="primary" @click="doSearch" :loading="searching" style="width: 100%">检索</el-button> |
||||
|
|
||||
|
<div v-if="results.length > 0" style="margin-top: 16px"> |
||||
|
<div v-for="(r, i) in results" :key="i" class="result-item"> |
||||
|
<div class="result-meta">相关度: {{ (r.score * 100).toFixed(1) }}% | 来源: {{ r.source }}</div> |
||||
|
<div class="result-content">{{ r.content }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<el-empty v-else-if="searched" description="未找到相关内容" :image-size="60" /> |
||||
|
</el-card> |
||||
|
|
||||
|
<el-card style="margin-top: 20px"> |
||||
|
<template #header>手动索引</template> |
||||
|
<el-input v-model="manualText" type="textarea" :rows="4" placeholder="输入文本内容" /> |
||||
|
<el-input v-model="manualSource" placeholder="来源标识" style="margin-top: 8px" /> |
||||
|
<el-button type="primary" @click="indexText" :loading="indexing" style="width: 100%; margin-top: 8px">索引文本</el-button> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, onMounted } from 'vue' |
||||
|
import { ElMessage, ElMessageBox } from 'element-plus' |
||||
|
import { ragApi } from '@/api' |
||||
|
|
||||
|
const loading = ref(false) |
||||
|
const searching = ref(false) |
||||
|
const indexing = ref(false) |
||||
|
const searched = ref(false) |
||||
|
const documents = ref<any[]>([]) |
||||
|
const query = ref('') |
||||
|
const results = ref<any[]>([]) |
||||
|
const manualText = ref('') |
||||
|
const manualSource = ref('manual') |
||||
|
|
||||
|
async function loadDocs() { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const res: any = await ragApi.search('__list_all__', 100) |
||||
|
const data = res?.data || [] |
||||
|
const seen = new Set<string>() |
||||
|
documents.value = (Array.isArray(data) ? data : []).filter((d: any) => { |
||||
|
if (seen.has(d.source)) return false |
||||
|
seen.add(d.source) |
||||
|
return true |
||||
|
}).map((d: any) => ({ |
||||
|
file_name: d.source, |
||||
|
file_type: d.source?.split('.').pop() || 'txt', |
||||
|
chunk_count: 1, |
||||
|
indexed_at: new Date().toISOString(), |
||||
|
})) |
||||
|
} finally { loading.value = false } |
||||
|
} |
||||
|
|
||||
|
async function uploadDoc(options: any) { |
||||
|
try { |
||||
|
await ragApi.upload(options.file) |
||||
|
ElMessage.success('文档已上传并索引') |
||||
|
await loadDocs() |
||||
|
} catch { ElMessage.error('上传失败') } |
||||
|
} |
||||
|
|
||||
|
async function doSearch() { |
||||
|
if (!query.value) return |
||||
|
searching.value = true |
||||
|
searched.value = true |
||||
|
try { |
||||
|
const res: any = await ragApi.search(query.value, 5) |
||||
|
results.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) |
||||
|
} finally { searching.value = false } |
||||
|
} |
||||
|
|
||||
|
async function indexText() { |
||||
|
if (!manualText.value) { ElMessage.warning('请输入文本'); return } |
||||
|
indexing.value = true |
||||
|
try { |
||||
|
await ragApi.indexText({ text: manualText.value, source: manualSource.value || 'manual' }) |
||||
|
ElMessage.success('文本已索引') |
||||
|
manualText.value = '' |
||||
|
await loadDocs() |
||||
|
} finally { indexing.value = false } |
||||
|
} |
||||
|
|
||||
|
async function deleteDoc(row: any) { |
||||
|
try { |
||||
|
await ElMessageBox.confirm(`确认删除 "${row.file_name}" 的索引?`, '提示', { type: 'warning' }) |
||||
|
ElMessage.success('索引已删除') |
||||
|
await loadDocs() |
||||
|
} catch { /**/ } |
||||
|
} |
||||
|
|
||||
|
onMounted(loadDocs) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; } |
||||
|
.result-item { padding: 10px 0; border-bottom: 1px solid #f0f0f0; } |
||||
|
.result-meta { font-size: 12px; color: #909399; margin-bottom: 4px; } |
||||
|
.result-content { font-size: 13px; line-height: 1.6; color: #303133; max-height: 80px; overflow: hidden; } |
||||
|
</style> |
||||
Loading…
Reference in new issue