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 |
|||
|
|||
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> |
|||
@ -1,64 +1,164 @@ |
|||
<template> |
|||
<div class="wecom-page"> |
|||
<el-card> |
|||
<template #header> |
|||
<span>企业微信配置</span> |
|||
</template> |
|||
<el-row :gutter="20"> |
|||
<el-col :span="14"> |
|||
<el-card> |
|||
<template #header> |
|||
<div class="card-header"> |
|||
<span>企业微信配置</span> |
|||
<el-tag :type="config?.status === 'active' ? 'success' : 'info'"> |
|||
{{ config?.status === 'active' ? '已连接' : '未配置' }} |
|||
</el-tag> |
|||
</div> |
|||
</template> |
|||
|
|||
<el-descriptions :column="1" border> |
|||
<el-descriptions-item label="机器人名称">{{ config?.bot_name || '企业AI助手' }}</el-descriptions-item> |
|||
<el-descriptions-item label="状态"> |
|||
<el-tag type="success">已配置</el-tag> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="支持功能"> |
|||
<el-tag v-for="f in config?.features" :key="f" style="margin-right: 8px">{{ f }}</el-tag> |
|||
</el-descriptions-item> |
|||
</el-descriptions> |
|||
<el-form :model="form" label-width="120px"> |
|||
<el-alert |
|||
title="企业微信集成" |
|||
type="info" |
|||
description="请在企业微信管理后台创建应用,获取 CorpID、Secret,并配置回调URL指向本服务的 /api/wecom/callback" |
|||
show-icon |
|||
:closable="false" |
|||
style="margin-bottom: 20px" |
|||
/> |
|||
<el-form-item label="CorpID" required> |
|||
<el-input v-model="form.corp_id" placeholder="企业微信 CorpID" /> |
|||
</el-form-item> |
|||
<el-form-item label="Secret" required> |
|||
<el-input v-model="form.secret" type="password" show-password placeholder="应用 Secret" /> |
|||
</el-form-item> |
|||
<el-form-item label="Agent ID"> |
|||
<el-input-number v-model="form.agent_id" :min="0" :max="9999999" style="width: 100%" /> |
|||
</el-form-item> |
|||
<el-form-item label="Token"> |
|||
<el-input v-model="form.token" placeholder="回调验证 Token" /> |
|||
</el-form-item> |
|||
<el-form-item label="AES Key"> |
|||
<el-input v-model="form.encoding_aes_key" placeholder="消息加密 AES Key" /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" @click="saveConfig" :loading="saving">保存配置</el-button> |
|||
<el-button @click="testConnection" :loading="testing">测试连接</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
</el-col> |
|||
|
|||
<el-divider /> |
|||
<el-col :span="10"> |
|||
<el-card> |
|||
<template #header>发送消息</template> |
|||
<el-form label-width="80px"> |
|||
<el-form-item label="目标用户"> |
|||
<el-input v-model="msgForm.to_user" placeholder="@all 或用户ID" /> |
|||
</el-form-item> |
|||
<el-form-item label="消息类型"> |
|||
<el-select v-model="msgForm.msg_type" style="width: 100%"> |
|||
<el-option label="文本" value="text" /> |
|||
<el-option label="文本卡片" value="textcard" /> |
|||
<el-option label="Markdown" value="markdown" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item label="消息内容"> |
|||
<el-input v-model="msgForm.content" type="textarea" :rows="4" placeholder="输入消息内容" /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" @click="sendMessage" :loading="sending">发送</el-button> |
|||
</el-form-item> |
|||
</el-form> |
|||
</el-card> |
|||
|
|||
<el-form label-width="120px"> |
|||
<el-alert |
|||
title="企业微信集成" |
|||
type="info" |
|||
description="请在企业微信管理后台创建应用,获取 CorpID、Secret,并配置回调URL指向本服务的 /api/wecom/callback" |
|||
show-icon |
|||
:closable="false" |
|||
style="margin-bottom: 20px" |
|||
/> |
|||
<el-form-item label="CorpID"> |
|||
<el-input placeholder="企业微信 CorpID" /> |
|||
</el-form-item> |
|||
<el-form-item label="Secret"> |
|||
<el-input type="password" show-password placeholder="应用 Secret" /> |
|||
</el-form-item> |
|||
<el-form-item label="Token"> |
|||
<el-input placeholder="回调 Token" /> |
|||
</el-form-item> |
|||
<el-form-item label="AES Key"> |
|||
<el-input placeholder="消息加密 AES Key" /> |
|||
</el-form-item> |
|||
<el-form-item> |
|||
<el-button type="primary" @click="saveConfig">保存配置</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="机器人名称">{{ config?.bot_name || '企业AI助手' }}</el-descriptions-item> |
|||
<el-descriptions-item label="消息回调">/api/wecom/callback</el-descriptions-item> |
|||
<el-descriptions-item label="支持功能"> |
|||
<el-tag v-for="f in (config?.features || ['智能对话', '任务推送', '通知提醒'])" :key="f" style="margin-right: 6px" size="small">{{ f }}</el-tag> |
|||
</el-descriptions-item> |
|||
</el-descriptions> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted } from 'vue' |
|||
import { ref, reactive, onMounted } from 'vue' |
|||
import { ElMessage } from 'element-plus' |
|||
import { wecomApi } from '@/api' |
|||
|
|||
const saving = ref(false) |
|||
const testing = ref(false) |
|||
const sending = ref(false) |
|||
const config = ref<any>(null) |
|||
|
|||
const form = reactive({ |
|||
corp_id: '', |
|||
secret: '', |
|||
agent_id: 0, |
|||
token: '', |
|||
encoding_aes_key: '', |
|||
}) |
|||
|
|||
const msgForm = reactive({ |
|||
to_user: '@all', |
|||
msg_type: 'text', |
|||
content: '', |
|||
}) |
|||
|
|||
onMounted(async () => { |
|||
const res: any = await wecomApi.getConfig() |
|||
config.value = res?.data || res || {} |
|||
try { |
|||
const res: any = await wecomApi.getConfig() |
|||
const data = res?.data || res || {} |
|||
config.value = data |
|||
form.corp_id = data.corp_id || '' |
|||
form.secret = data.secret || '' |
|||
form.agent_id = data.agent_id || 0 |
|||
form.token = data.token || '' |
|||
form.encoding_aes_key = data.encoding_aes_key || '' |
|||
} catch { /**/ } |
|||
}) |
|||
|
|||
function saveConfig() { |
|||
ElMessage.success('配置已保存') |
|||
async function saveConfig() { |
|||
saving.value = true |
|||
try { |
|||
await wecomApi.updateConfig({ |
|||
corp_id: form.corp_id, |
|||
secret: form.secret, |
|||
agent_id: form.agent_id, |
|||
token: form.token, |
|||
encoding_aes_key: form.encoding_aes_key, |
|||
}) |
|||
ElMessage.success('配置已保存') |
|||
} catch { ElMessage.error('保存失败') } |
|||
finally { saving.value = false } |
|||
} |
|||
|
|||
async function testConnection() { |
|||
testing.value = true |
|||
try { |
|||
await wecomApi.getConfig() |
|||
ElMessage.success('连接测试成功') |
|||
} catch { ElMessage.error('连接失败') } |
|||
finally { testing.value = false } |
|||
} |
|||
|
|||
async function sendMessage() { |
|||
if (!msgForm.to_user || !msgForm.content) { ElMessage.warning('请填写目标用户和消息内容'); return } |
|||
sending.value = true |
|||
try { |
|||
await wecomApi.sendMessage({ |
|||
to_user: msgForm.to_user, |
|||
msg_type: msgForm.msg_type, |
|||
content: msgForm.content, |
|||
}) |
|||
ElMessage.success('消息已发送') |
|||
msgForm.content = '' |
|||
} catch { ElMessage.error('发送失败') } |
|||
finally { sending.value = false } |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.card-header { display: flex; justify-content: space-between; align-items: center; } |
|||
</style> |
|||
Loading…
Reference in new issue