From 3b5514e9b55c7c03c1cebac6407698e6eda0d86e Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 27 Mar 2026 01:15:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(apikey):=20=E4=BF=AE=E5=A4=8D=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E6=97=A0=E6=B3=95=E5=88=9B=E5=BB=BA=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=20API=5FKEY=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端:API Key 管理 tab 对所有登录用户可见,非管理员默认显示 API Key tab - 前端:apikey_api 改为普通 API 调用(移除 superadmin 专用接口) - 后端:list/create/get/update/delete/regenerate 接口改为按用户过滤,superadmin 可查看全部 --- backend/server/routers/apikey_router.py | 63 ++++++++++++++++++----- docs/develop-guides/roadmap.md | 1 + web/src/apis/apikey_api.js | 12 ++--- web/src/components/AgentChatComponent.vue | 34 ++++++++++++ web/src/components/AgentInputArea.vue | 2 +- web/src/components/SettingsModal.vue | 8 +-- 6 files changed, 96 insertions(+), 24 deletions(-) diff --git a/backend/server/routers/apikey_router.py b/backend/server/routers/apikey_router.py index f0206980..17202597 100644 --- a/backend/server/routers/apikey_router.py +++ b/backend/server/routers/apikey_router.py @@ -66,14 +66,29 @@ class APIKeyCreateResponse(BaseModel): async def list_api_keys( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): - """列出所有 API Keys""" - result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()).offset(skip).limit(limit)) - api_keys = result.scalars().all() - - total_result = await db.execute(select(func.count(APIKey.id))) + """列出当前用户的 API Keys""" + # 普通用户只能看到自己的 API Keys + if current_user.role == "superadmin": + # superadmin 可以看到所有 + result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()).offset(skip).limit(limit)) + api_keys = result.scalars().all() + total_result = await db.execute(select(func.count(APIKey.id))) + else: + # 普通用户只看自己的 + result = await db.execute( + select(APIKey) + .filter(APIKey.user_id == current_user.id) + .order_by(APIKey.created_at.desc()) + .offset(skip) + .limit(limit) + ) + api_keys = result.scalars().all() + total_result = await db.execute( + select(func.count(APIKey.id)).filter(APIKey.user_id == current_user.id) + ) total = total_result.scalar() return { @@ -92,6 +107,10 @@ async def create_api_key( # 生成 Key full_key, key_hash, key_prefix = generate_api_key() + # 普通用户只能为自己创建 API Key,不能指定其他用户 + if data.user_id and data.user_id != current_user.id and current_user.role != "superadmin": + raise HTTPException(status_code=403, detail="无权为其他用户创建 API Key") + # 验证关联用户 if data.user_id: result = await db.execute(select(User).filter(User.id == data.user_id)) @@ -133,16 +152,20 @@ async def create_api_key( @apikey_router.get("/{api_key_id}", response_model=dict) async def get_api_key( api_key_id: int, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): - """获取单个 API Key""" + """获取单个 API Key(只能操作自己的 Key)""" result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id)) api_key = result.scalar_one_or_none() if not api_key: raise HTTPException(status_code=404, detail="API Key 不存在") + # 检查权限:只能操作自己的 Key,或者 superadmin 可以操作所有 + if api_key.user_id != current_user.id and current_user.role != "superadmin": + raise HTTPException(status_code=403, detail="无权操作此 API Key") + return {"api_key": api_key.to_dict()} @@ -150,16 +173,20 @@ async def get_api_key( async def update_api_key( api_key_id: int, data: APIKeyUpdate, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): - """更新 API Key""" + """更新 API Key(只能操作自己的 Key)""" result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id)) api_key = result.scalar_one_or_none() if not api_key: raise HTTPException(status_code=404, detail="API Key 不存在") + # 检查权限:只能操作自己的 Key,或者 superadmin 可以操作所有 + if api_key.user_id != current_user.id and current_user.role != "superadmin": + raise HTTPException(status_code=403, detail="无权操作此 API Key") + if data.name is not None: api_key.name = data.name @@ -179,16 +206,20 @@ async def update_api_key( @apikey_router.delete("/{api_key_id}", response_model=dict) async def delete_api_key( api_key_id: int, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): - """删除 API Key""" + """删除 API Key(只能操作自己的 Key)""" result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id)) api_key = result.scalar_one_or_none() if not api_key: raise HTTPException(status_code=404, detail="API Key 不存在") + # 检查权限:只能操作自己的 Key,或者 superadmin 可以操作所有 + if api_key.user_id != current_user.id and current_user.role != "superadmin": + raise HTTPException(status_code=403, detail="无权操作此 API Key") + await db.delete(api_key) await db.commit() @@ -198,16 +229,20 @@ async def delete_api_key( @apikey_router.post("/{api_key_id}/regenerate", response_model=APIKeyCreateResponse) async def regenerate_api_key( api_key_id: int, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): - """重新生成 API Key 密钥(secret 仅在此处返回一次)""" + """重新生成 API Key 密钥(secret 仅在此处返回一次,只能操作自己的 Key)""" result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id)) api_key = result.scalar_one_or_none() if not api_key: raise HTTPException(status_code=404, detail="API Key 不存在") + # 检查权限:只能操作自己的 Key,或者 superadmin 可以操作所有 + if api_key.user_id != current_user.id and current_user.role != "superadmin": + raise HTTPException(status_code=403, detail="无权操作此 API Key") + # 生成新密钥 full_key, key_hash, key_prefix = generate_api_key() diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 65ad3b46..b650f83b 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -48,6 +48,7 @@ ### 修复 - 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试 +- 修复对话线程与 Agent 配置错位的问题:发送消息时将当前 `agent_config_id` 绑定到 thread 的 `extra_metadata`,线程列表接口返回该绑定值,前端切换历史 thread 时会自动恢复对应配置 - 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试 - 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标 - 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败 diff --git a/web/src/apis/apikey_api.js b/web/src/apis/apikey_api.js index 445dc78e..d2c35350 100644 --- a/web/src/apis/apikey_api.js +++ b/web/src/apis/apikey_api.js @@ -1,15 +1,15 @@ -import { apiSuperAdminGet, apiSuperAdminPost, apiSuperAdminPut, apiDelete } from './base' +import { apiGet, apiPost, apiPut, apiDelete } from './base' export const apikeyApi = { - list: (skip = 0, limit = 100) => apiSuperAdminGet('/api/apikey/', { params: { skip, limit } }), + list: (skip = 0, limit = 100) => apiGet('/api/apikey/', { params: { skip, limit } }), - create: (data) => apiSuperAdminPost('/api/apikey/', data), + create: (data) => apiPost('/api/apikey/', data), - get: (id) => apiSuperAdminGet(`/api/apikey/${id}`), + get: (id) => apiGet(`/api/apikey/${id}`), - update: (id, data) => apiSuperAdminPut(`/api/apikey/${id}`, data), + update: (id, data) => apiPut(`/api/apikey/${id}`, data), delete: (id) => apiDelete(`/api/apikey/${id}`), - regenerate: (id) => apiSuperAdminPost(`/api/apikey/${id}/regenerate`) + regenerate: (id) => apiPost(`/api/apikey/${id}/regenerate`) } diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 6bb75830..b9c9812c 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -622,6 +622,30 @@ onUnmounted(() => { }) // ==================== 线程管理方法 ==================== +const setThreadAgentConfigId = (threadId, agentConfigId) => { + if (!threadId) return + const thread = threads.value.find((item) => item.id === threadId) + if (thread) { + thread.agent_config_id = agentConfigId ?? null + } +} + +const syncSelectedConfigForThread = async (thread) => { + if (!thread?.agent_config_id) return + + const targetAgentId = thread.agent_id || currentAgentId.value + if (!targetAgentId) return + + const configList = agentStore.agentConfigs[targetAgentId] || [] + if (!configList.length) { + await agentStore.fetchAgentConfigs(targetAgentId) + } + + if (selectedAgentConfigId.value !== thread.agent_config_id) { + await agentStore.selectAgentConfig(thread.agent_config_id) + } +} + // 获取当前智能体的线程列表 const fetchThreads = async (agentId = null) => { const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId @@ -926,6 +950,8 @@ const sendMessage = async ({ return Promise.reject(error) } + setThreadAgentConfigId(threadId, selectedAgentConfigId.value) + const requestData = { query: text, thread_id: threadId, @@ -1001,6 +1027,14 @@ const selectChat = async (chatId) => { } } + try { + await syncSelectedConfigForThread(targetChat) + } catch (error) { + chatState.currentThreadId = previousThreadId + handleChatError(error, 'load') + return + } + chatUIStore.isLoadingMessages = true try { await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId }) diff --git a/web/src/components/AgentInputArea.vue b/web/src/components/AgentInputArea.vue index 1505b803..81f5ff5f 100644 --- a/web/src/components/AgentInputArea.vue +++ b/web/src/components/AgentInputArea.vue @@ -148,7 +148,7 @@ defineExpose({ display: flex; align-items: center; gap: 6px; - padding: 8px 8px; + padding: 6px 8px; // height: 28px; border-radius: 8px; font-size: 14px; diff --git a/web/src/components/SettingsModal.vue b/web/src/components/SettingsModal.vue index 51f58c0e..c1f40817 100644 --- a/web/src/components/SettingsModal.vue +++ b/web/src/components/SettingsModal.vue @@ -58,7 +58,7 @@ class="sider-item" :class="{ activesec: activeTab === 'apikey' }" @click="activeTab = 'apikey'" - v-if="userStore.isSuperAdmin" + v-if="userStore.isLoggedIn" > API Key @@ -103,7 +103,7 @@ class="nav-item" :class="{ active: activeTab === 'apikey' }" @click="activeTab = 'apikey'" - v-if="userStore.isSuperAdmin" + v-if="userStore.isLoggedIn" > API Key @@ -128,7 +128,7 @@ -
+
@@ -175,6 +175,8 @@ watch( if (newVal) { if (userStore.isAdmin) { activeTab.value = 'base' + } else if (userStore.isLogin) { + activeTab.value = 'apikey' } } }