fix(apikey): 修复管理员无法创建维护 API_KEY 的问题

- 前端:API Key 管理 tab 对所有登录用户可见,非管理员默认显示 API Key tab
- 前端:apikey_api 改为普通 API 调用(移除 superadmin 专用接口)
- 后端:list/create/get/update/delete/regenerate 接口改为按用户过滤,superadmin 可查看全部
This commit is contained in:
Wenjie Zhang 2026-03-27 01:15:42 +08:00
parent efecc0692d
commit 3b5514e9b5
6 changed files with 96 additions and 24 deletions

View File

@ -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()

View File

@ -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 在依赖缓存和安装阶段直接失败

View File

@ -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`)
}

View File

@ -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 })

View File

@ -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;

View File

@ -58,7 +58,7 @@
class="sider-item"
:class="{ activesec: activeTab === 'apikey' }"
@click="activeTab = 'apikey'"
v-if="userStore.isSuperAdmin"
v-if="userStore.isLoggedIn"
>
<KeyIcon class="icon" :size="18" />
<span>API Key</span>
@ -103,7 +103,7 @@
class="nav-item"
:class="{ active: activeTab === 'apikey' }"
@click="activeTab = 'apikey'"
v-if="userStore.isSuperAdmin"
v-if="userStore.isLoggedIn"
>
API Key
</div>
@ -128,7 +128,7 @@
<DepartmentManagementComponent />
</div>
<div v-show="activeTab === 'apikey'" v-if="userStore.isSuperAdmin">
<div v-show="activeTab === 'apikey'" v-if="userStore.isLoggedIn">
<ApiKeyManagementComponent />
</div>
</div>
@ -175,6 +175,8 @@ watch(
if (newVal) {
if (userStore.isAdmin) {
activeTab.value = 'base'
} else if (userStore.isLogin) {
activeTab.value = 'apikey'
}
}
}