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"
>