diff --git a/PLAN2.md b/PLAN2.md new file mode 100644 index 0000000..7a1d859 --- /dev/null +++ b/PLAN2.md @@ -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 | \ No newline at end of file diff --git a/README.md b/README.md index 7befa28..4a70ee7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # hg-agents +sroot admin123 root ( *:* ) 无条件测试所有功能 +admin admin123 super_admin 系统管理员测试 +manager manager123 dept_manager 部门经理权限测试 +employee employee123 employee 普通员工权限测试 + diff --git a/backend/middleware/rbac_middleware.py b/backend/middleware/rbac_middleware.py index afa0620..85cb715 100644 --- a/backend/middleware/rbac_middleware.py +++ b/backend/middleware/rbac_middleware.py @@ -8,7 +8,7 @@ from sqlalchemy import select async def rbac_middleware(request: Request, call_next): - public_paths = ["/api/auth/login", "/health", "/docs", "/openapi.json", "/wecom/callback"] + public_paths = ["/api/auth/login", "/api/auth/wecom", "/health", "/docs", "/openapi.json", "/wecom/callback"] if any(request.url.path.startswith(p) for p in public_paths): return await call_next(request) @@ -33,6 +33,9 @@ async def rbac_middleware(request: Request, call_next): ) roles = ur_result.scalars().all() + role_codes = [r.code for r in roles] + is_root = "root" in role_codes + permissions = [] data_scopes = [] for role in roles: @@ -43,14 +46,20 @@ async def rbac_middleware(request: Request, call_next): perms = rp_result.scalars().all() permissions.extend([p.code for p in perms]) + unique_perms = list(set(permissions)) + + if is_root and "*:*" not in unique_perms: + unique_perms.insert(0, "*:*") + request.state.user = { "id": str(user.id), "username": user.username, "display_name": user.display_name, "department_id": str(user.department_id) if user.department_id else None, - "role": roles[0].code if roles else "employee", - "permissions": list(set(permissions)), - "data_scope": "all" if "all" in data_scopes else ( + "roles": [{"code": r.code, "name": r.name, "data_scope": r.data_scope} for r in roles], + "permissions": unique_perms, + "is_root": is_root, + "data_scope": "all" if is_root or "all" in data_scopes else ( "subordinate_only" if "subordinate_only" in data_scopes else "self_only" ), } diff --git a/backend/modules/auth/router.py b/backend/modules/auth/router.py index ab1eb21..8788592 100644 --- a/backend/modules/auth/router.py +++ b/backend/modules/auth/router.py @@ -104,5 +104,59 @@ async def get_wecom_oauth_url(): return {"code": 200, "data": {"url": url}} +@router.put("/me") +async def update_me( + request: Request, + payload: dict, + db: AsyncSession = Depends(get_db), +): + user_ctx = request.state.user + result = await db.execute(select(User).where(User.id == user_ctx["id"])) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(404, "用户不存在") + + if "display_name" in payload: + user.display_name = payload["display_name"] + if "email" in payload: + user.email = payload["email"] + if "phone" in payload: + user.phone = payload["phone"] + + await db.commit() + roles = await get_user_roles(db, user.id) + return UserOut( + id=user.id, username=user.username, display_name=user.display_name, + email=user.email, phone=user.phone, wecom_user_id=user.wecom_user_id, + department_id=user.department_id, position=user.position, + manager_id=user.manager_id, status=user.status, + roles=roles, created_at=user.created_at, + ) + + +@router.put("/password") +async def change_password( + request: Request, + payload: dict, + db: AsyncSession = Depends(get_db), +): + user_ctx = request.state.user + result = await db.execute(select(User).where(User.id == user_ctx["id"])) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(404, "用户不存在") + + old_pw = payload.get("old_password", "") + new_pw = payload.get("new_password", "") + if not bcrypt.checkpw(old_pw.encode('utf-8'), user.password_hash.encode('utf-8')): + raise HTTPException(400, "当前密码错误") + if len(new_pw) < 6: + raise HTTPException(400, "新密码至少6位") + + user.password_hash = hash_password(new_pw) + await db.commit() + return {"code": 200, "message": "密码已修改"} + + def hash_password(password: str) -> str: return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') \ No newline at end of file diff --git a/backend/modules/task/router.py b/backend/modules/task/router.py index fffe9b7..b83b316 100644 --- a/backend/modules/task/router.py +++ b/backend/modules/task/router.py @@ -97,6 +97,17 @@ async def create_task(req: TaskCreate, request: Request, db: AsyncSession = Depe ) +@router.delete("/{task_id}") +async def delete_task(task_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(Task).where(Task.id == task_id)) + task = result.scalar_one_or_none() + if not task: + raise HTTPException(404, "任务不存在") + await db.delete(task) + await db.commit() + return {"code": 200, "message": "任务已删除"} + + @router.get("/{task_id}", response_model=TaskOut) async def get_task(task_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)): result = await db.execute(select(Task).where(Task.id == task_id)) diff --git a/backend/modules/wecom/router.py b/backend/modules/wecom/router.py index eac2b66..a2c7d5d 100644 --- a/backend/modules/wecom/router.py +++ b/backend/modules/wecom/router.py @@ -159,7 +159,63 @@ async def get_wecom_config(request: Request): "code": 200, "data": { "bot_name": "企业AI助手", - "status": "configured", + "status": "active" if settings.WECOM_CORP_ID else "unconfigured", + "corp_id": settings.WECOM_CORP_ID or "", + "agent_id": getattr(settings, 'WECOM_AGENT_ID', 0), "features": ["消息对话", "文件处理", "任务通知", "工作流触发"], }, - } \ No newline at end of file + } + + +@router.put("/config") +async def update_wecom_config(request: Request, payload: dict): + import os + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env') + updates = {} + if "corp_id" in payload: + updates["WECOM_CORP_ID"] = payload["corp_id"] + if "secret" in payload: + updates["WECOM_APP_SECRET"] = payload["secret"] + if "agent_id" in payload: + updates["WECOM_AGENT_ID"] = str(payload["agent_id"]) + if "token" in payload: + updates["WECOM_TOKEN"] = payload["token"] + if "encoding_aes_key" in payload: + updates["WECOM_ENCODING_AES_KEY"] = payload["encoding_aes_key"] + + if updates: + lines = [] + if os.path.exists(env_path): + with open(env_path, 'r') as f: + lines = f.readlines() + existing_keys = {l.split('=')[0].strip() for l in lines if '=' in l and not l.startswith('#')} + for key, value in updates.items(): + if key in existing_keys: + lines = [f"{key}={value}\n" if l.split('=')[0].strip() == key else l for l in lines] + else: + lines.append(f"{key}={value}\n") + with open(env_path, 'w') as f: + f.writelines(lines) + + for key, value in updates.items(): + if hasattr(settings, key): + setattr(settings, key, value) + + return {"code": 200, "message": "配置已保存"} + + +@router.post("/send") +async def send_wecom_message(request: Request, payload: dict): + to_user = payload.get("to_user", "@all") + msg_type = payload.get("msg_type", "text") + content = payload.get("content", "") + + if not content: + return {"code": 400, "message": "消息内容不能为空"} + + try: + from agentscope_integration.tools.wecom_tools import send_notification + result = send_notification(to_user, content, msg_type) + return {"code": 200, "message": result} + except Exception as e: + return {"code": 500, "message": f"发送失败: {e}"} \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 3525c81..1b78f62 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -67,6 +67,7 @@ export const taskApi = { createTask: (data: any) => api.post('/tasks', data), getTask: (id: string) => api.get(`/tasks/${id}`), updateTask: (id: string, data: any) => api.put(`/tasks/${id}`, data), + deleteTask: (id: string) => api.delete(`/tasks/${id}`), pushTask: (id: string) => api.post(`/tasks/${id}/push`), } @@ -86,6 +87,7 @@ export const flowApi = { export const wecomApi = { sendMessage: (data: any) => api.post('/wecom/send', data), getConfig: () => api.get('/wecom/config'), + updateConfig: (data: any) => api.put('/wecom/config', data), } export const agentApi = { @@ -96,8 +98,12 @@ export const agentApi = { export const mcpApi = { getServers: () => api.get('/mcp/servers'), + createServer: (data: any) => api.post('/mcp/servers', data), registerServer: (data: any) => api.post('/mcp/servers', data), + updateServer: (id: string, data: any) => api.put(`/mcp/servers/${id}`, data), + testServer: (id: string) => api.post(`/mcp/servers/${id}/test`), testConnection: (id: string) => api.post(`/mcp/servers/${id}/test`), + deleteServer: (id: string) => api.delete(`/mcp/servers/${id}`), unregisterServer: (id: string) => api.delete(`/mcp/servers/${id}`), } diff --git a/frontend/src/components/layout/AdminLayout.vue b/frontend/src/components/layout/AdminLayout.vue index a7a1fef..ebf0969 100644 --- a/frontend/src/components/layout/AdminLayout.vue +++ b/frontend/src/components/layout/AdminLayout.vue @@ -43,12 +43,32 @@ 流列表 流编辑器 流市场 + 流详情 - - - 创建任务 - + + + + MCP服务 + + 服务管理 + + + + + + 知识库 + + 知识库管理 + + + + + + 任务管理 + + 创建任务 + @@ -106,7 +126,7 @@ import { ref, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' -import { Fold, User, ArrowDown } from '@element-plus/icons-vue' +import { Fold, User, ArrowDown, List, Connection, Search } from '@element-plus/icons-vue' const route = useRoute() const router = useRouter() diff --git a/frontend/src/components/layout/MainLayout.vue b/frontend/src/components/layout/MainLayout.vue index d33f2e4..2981b1a 100644 --- a/frontend/src/components/layout/MainLayout.vue +++ b/frontend/src/components/layout/MainLayout.vue @@ -44,6 +44,11 @@ 文档管理 + + + 知识库 + + 企微配置 @@ -54,12 +59,17 @@ 通知中心 + + + 个人中心 + + 系统配置 - + 管理后台 @@ -102,7 +112,7 @@ import { ref, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' -import { Fold, User, ArrowDown, Tools } from '@element-plus/icons-vue' +import { Fold, User, ArrowDown, Tools, Search } from '@element-plus/icons-vue' const route = useRoute() const router = useRouter() diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 9f7feb1..f58582a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -68,6 +68,12 @@ const router = createRouter({ component: () => import('@/views/document/DocumentManager.vue'), meta: { title: '文档管理' }, }, + { + path: 'rag/knowledge', + name: 'KnowledgeBase', + component: () => import('@/views/rag/KnowledgeBase.vue'), + meta: { title: '知识库' }, + }, { path: 'wecom/config', name: 'WecomConfig', @@ -80,6 +86,12 @@ const router = createRouter({ component: () => import('@/views/notification/NotificationCenter.vue'), meta: { title: '通知中心' }, }, + { + path: 'profile', + name: 'Profile', + component: () => import('@/views/profile/Profile.vue'), + meta: { title: '个人中心' }, + }, { path: 'settings', name: 'Settings', @@ -147,6 +159,24 @@ const router = createRouter({ component: () => import('@/views/flow/FlowMarket.vue'), meta: { title: '流市场', perms: ['flow:read'] }, }, + { + path: 'flow/market/:id', + name: 'AdminFlowDetail', + component: () => import('@/views/flow/FlowDetail.vue'), + meta: { title: '流详情', perms: ['flow:read'] }, + }, + { + path: 'mcp/manager', + name: 'AdminMcpManager', + component: () => import('@/views/mcp/McpManager.vue'), + meta: { title: 'MCP服务管理', perms: ['flow:create'] }, + }, + { + path: 'rag/knowledge', + name: 'AdminKnowledgeBase', + component: () => import('@/views/rag/KnowledgeBase.vue'), + meta: { title: '知识库管理', perms: ['flow:create'] }, + }, { path: 'task/create', name: 'AdminTaskCreate', @@ -170,21 +200,31 @@ const router = createRouter({ ], }) -router.beforeEach((to, _from, next) => { +router.beforeEach(async (to, _from, next) => { const userStore = useUserStore() - if (to.name !== 'Login' && !userStore.token) { + + if (!userStore.token) { + if (to.name === 'Login') { next(); return } next({ name: 'Login', query: { redirect: to.fullPath } }) - } else if (to.meta.perms && Array.isArray(to.meta.perms) && to.meta.perms.length > 0) { - const userPerms = userStore.permissions - const hasPerm = userPerms.includes('*:*') || to.meta.perms.some((p: string) => userPerms.includes(p)) - if (!hasPerm) { + return + } + + if (!userStore.user) { + await userStore.fetchUser() + if (!userStore.isLoggedIn) { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + } + + if (to.meta.perms && Array.isArray(to.meta.perms) && to.meta.perms.length > 0) { + if (!userStore.hasPermission(to.meta.perms[0])) { next('/user/dashboard') - } else { - next() + return } - } else { - next() } + + next() }) export default router \ No newline at end of file diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index 7db7940..19b5832 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -1,20 +1,53 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' +import api from '@/api' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') - const user = ref(null) - const permissions = ref([]) + const user = ref(JSON.parse(localStorage.getItem('user') || 'null')) + const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]')) const isLoggedIn = computed(() => !!token.value) const displayName = computed(() => user.value?.display_name || '') - const role = computed(() => user.value?.roles?.[0]?.code || '') + const username = computed(() => user.value?.username || '') + const roleCodes = computed(() => (user.value?.roles || []).map((r: any) => r.code || r)) + const isSuperAdmin = computed(() => { + if (roleCodes.value.includes('root')) return true + if (permissions.value.includes('*:*')) return true + return false + }) + const isAdmin = computed(() => { + return isSuperAdmin.value || roleCodes.value.includes('super_admin') + }) function setAuth(t: string, u: any) { token.value = t user.value = u - permissions.value = u?.roles?.flatMap((r: any) => r.permissions || []) || [] + const perms = u?.roles?.flatMap((r: any) => + (r.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code) + ) || [] + permissions.value = perms + localStorage.setItem('token', t) + localStorage.setItem('user', JSON.stringify(u)) + localStorage.setItem('permissions', JSON.stringify(perms)) + } + + async function fetchUser() { + if (!token.value) return + try { + const res = await api.get('/auth/me') + const u = res?.data || res || {} + user.value = u + const perms = u?.roles?.flatMap((r: any) => + (r.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code) + ) || [] + permissions.value = perms + localStorage.setItem('user', JSON.stringify(u)) + localStorage.setItem('permissions', JSON.stringify(perms)) + } catch { + logout() + } } function logout() { @@ -22,11 +55,19 @@ export const useUserStore = defineStore('user', () => { user.value = null permissions.value = [] localStorage.removeItem('token') + localStorage.removeItem('user') + localStorage.removeItem('permissions') } function hasPermission(code: string): boolean { - return permissions.value.includes('*:*') || permissions.value.includes(code) + if (isSuperAdmin.value) return true + if (roleCodes.value.includes('super_admin')) return true + return permissions.value.includes(code) } - return { token, user, permissions, isLoggedIn, displayName, role, setAuth, logout, hasPermission } + return { + token, user, permissions, + isLoggedIn, displayName, username, roleCodes, isSuperAdmin, isAdmin, + setAuth, fetchUser, logout, hasPermission, + } }) \ No newline at end of file diff --git a/frontend/src/views/agent/AgentChat.vue b/frontend/src/views/agent/AgentChat.vue index c9afce6..79450ec 100644 --- a/frontend/src/views/agent/AgentChat.vue +++ b/frontend/src/views/agent/AgentChat.vue @@ -1,26 +1,56 @@ - + + + + + + + + {{ agentConfig.name }} + {{ agentConfig.desc }} + + + {{ agentConfig.tag }} + + + + + {{ msg.content }} {{ new Date(msg.created_at).toLocaleTimeString() }} + + + - AI思考中... + {{ agentConfig.name }}思考中... + + 你可以试试: + {{ s }} + + route.params.type as string) -const agentName = computed(() => { - const names: Record = { employee: '员工AI助手', manager: '管理分析助手', task: '任务管理助手', document: '文档处理助手' } - return names[agentType.value] || agentType.value -}) + +const agentConfigs: Record = { + employee: { + name: '员工AI助手', + desc: '你的专属工作助手,帮你处理日常事务', + color: '#409EFF', + icon: ChatDotRound, + tagType: 'primary', + tag: '日常办公', + placeholder: '问我任何工作相关的问题...', + suggestions: ['帮我整理今天的工作计划', '查询我的待办任务', '帮我修正一份文档的格式', '发送通知给同事'], + }, + manager: { + name: '管理分析助手', + desc: '帮你分析团队数据,生成管理报告', + color: '#67C23A', + icon: TrendCharts, + tagType: 'success', + tag: '管理决策', + placeholder: '输入你想了解的团队数据...', + suggestions: ['查看下属员工列表', '生成本周团队效率报告', '分析任务完成情况', '查看员工工作看板'], + }, + task: { + name: '任务管理助手', + desc: '帮你创建、跟踪和管理工作任务', + color: '#E6A23C', + icon: List, + tagType: 'warning', + tag: '任务跟踪', + placeholder: '告诉我你要创建或查询的任务...', + suggestions: ['创建一个新任务', '查询当前所有任务', '更新任务状态为已完成', '推送任务提醒到企微'], + }, + document: { + name: '文档处理助手', + desc: '帮你解析、检索和处理各类文档', + color: '#337ecc', + icon: Document, + tagType: 'info', + tag: '文档处理', + placeholder: '上传文档或描述你的文档需求...', + suggestions: ['帮我解析一份PDF文档', '从知识库检索相关内容', '修正文档格式', '提取文档关键信息'], + }, +} + +const agentConfig = computed(() => agentConfigs[agentType.value] || agentConfigs.employee) const messages = ref([]) const inputText = ref('') @@ -55,7 +126,7 @@ const sessionId = ref(`session_${Date.now()}`) onMounted(() => { messages.value.push({ role: 'assistant', - content: `你好!我是${agentName.value},有什么可以帮助你的?`, + content: `你好!我是${agentConfig.value.name},${agentConfig.value.desc}。有什么可以帮助你的?`, created_at: new Date().toISOString(), }) }) @@ -83,6 +154,11 @@ async function sendMessage() { } } +function quickSend(text: string) { + inputText.value = text + sendMessage() +} + async function scrollBottom() { await nextTick() if (msgContainer.value) { @@ -97,6 +173,27 @@ async function scrollBottom() { flex-direction: column; height: calc(100vh - 180px); } +.chat-header-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 12px; + border-bottom: 1px solid #ebeef5; + margin-bottom: 12px; +} +.agent-info { + display: flex; + align-items: center; + gap: 12px; +} +.agent-avatar { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; +} .chat-messages { flex: 1; overflow-y: auto; @@ -106,12 +203,20 @@ async function scrollBottom() { .msg-item { margin-bottom: 16px; display: flex; + align-items: flex-start; + gap: 8px; } .msg-item.user { justify-content: flex-end; } -.msg-item.assistant { - justify-content: flex-start; +.msg-avatar-small { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } .msg-bubble { max-width: 70%; @@ -133,6 +238,24 @@ async function scrollBottom() { opacity: 0.6; margin-top: 4px; } +.chat-suggestions { + padding: 8px 0; + border-top: 1px solid #ebeef5; + margin-bottom: 8px; +} +.suggestion-title { + font-size: 12px; + color: #909399; + margin-bottom: 8px; +} +.suggestion-tag { + cursor: pointer; + margin: 0 6px 6px 0; +} +.suggestion-tag:hover { + color: #409EFF; + border-color: #409EFF; +} .chat-input { display: flex; align-items: flex-end; diff --git a/frontend/src/views/flow/FlowDetail.vue b/frontend/src/views/flow/FlowDetail.vue new file mode 100644 index 0000000..4adaa61 --- /dev/null +++ b/frontend/src/views/flow/FlowDetail.vue @@ -0,0 +1,104 @@ + + + + + + + + 流定义 + + {{ flow?.name }} + + {{ flow?.status }} + + {{ flow?.created_by || '-' }} + {{ flow?.created_at ? new Date(flow.created_at).toLocaleString() : '-' }} + {{ flow?.description || '无' }} + + + 节点列表 + + + + {{ row.type }} + + + + + + + + + 操作 + + 安装到我的工作流 + + + 测试运行 + + + 编辑此流 + + + + + 连接关系 + + {{ edge.source }} → {{ edge.target }} + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/views/flow/FlowMarket.vue b/frontend/src/views/flow/FlowMarket.vue index 6f99174..65d52e8 100644 --- a/frontend/src/views/flow/FlowMarket.vue +++ b/frontend/src/views/flow/FlowMarket.vue @@ -7,7 +7,7 @@ - + {{ flow.name }} v{{ flow.version }} @@ -15,6 +15,7 @@ {{ flow.description || '暂无描述' }}
{{ flow.description || '暂无描述' }}