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:
parent
efecc0692d
commit
3b5514e9b5
@ -66,14 +66,29 @@ class APIKeyCreateResponse(BaseModel):
|
|||||||
async def list_api_keys(
|
async def list_api_keys(
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=500),
|
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),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""列出所有 API Keys"""
|
"""列出当前用户的 API Keys"""
|
||||||
result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()).offset(skip).limit(limit))
|
# 普通用户只能看到自己的 API Keys
|
||||||
api_keys = result.scalars().all()
|
if current_user.role == "superadmin":
|
||||||
|
# superadmin 可以看到所有
|
||||||
total_result = await db.execute(select(func.count(APIKey.id)))
|
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()
|
total = total_result.scalar()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -92,6 +107,10 @@ async def create_api_key(
|
|||||||
# 生成 Key
|
# 生成 Key
|
||||||
full_key, key_hash, key_prefix = generate_api_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:
|
if data.user_id:
|
||||||
result = await db.execute(select(User).filter(User.id == 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)
|
@apikey_router.get("/{api_key_id}", response_model=dict)
|
||||||
async def get_api_key(
|
async def get_api_key(
|
||||||
api_key_id: int,
|
api_key_id: int,
|
||||||
current_user: User = Depends(get_superadmin_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取单个 API Key"""
|
"""获取单个 API Key(只能操作自己的 Key)"""
|
||||||
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
||||||
api_key = result.scalar_one_or_none()
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise HTTPException(status_code=404, detail="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()}
|
return {"api_key": api_key.to_dict()}
|
||||||
|
|
||||||
|
|
||||||
@ -150,16 +173,20 @@ async def get_api_key(
|
|||||||
async def update_api_key(
|
async def update_api_key(
|
||||||
api_key_id: int,
|
api_key_id: int,
|
||||||
data: APIKeyUpdate,
|
data: APIKeyUpdate,
|
||||||
current_user: User = Depends(get_superadmin_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""更新 API Key"""
|
"""更新 API Key(只能操作自己的 Key)"""
|
||||||
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
||||||
api_key = result.scalar_one_or_none()
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise HTTPException(status_code=404, detail="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:
|
if data.name is not None:
|
||||||
api_key.name = data.name
|
api_key.name = data.name
|
||||||
|
|
||||||
@ -179,16 +206,20 @@ async def update_api_key(
|
|||||||
@apikey_router.delete("/{api_key_id}", response_model=dict)
|
@apikey_router.delete("/{api_key_id}", response_model=dict)
|
||||||
async def delete_api_key(
|
async def delete_api_key(
|
||||||
api_key_id: int,
|
api_key_id: int,
|
||||||
current_user: User = Depends(get_superadmin_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""删除 API Key"""
|
"""删除 API Key(只能操作自己的 Key)"""
|
||||||
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
||||||
api_key = result.scalar_one_or_none()
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise HTTPException(status_code=404, detail="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.delete(api_key)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -198,16 +229,20 @@ async def delete_api_key(
|
|||||||
@apikey_router.post("/{api_key_id}/regenerate", response_model=APIKeyCreateResponse)
|
@apikey_router.post("/{api_key_id}/regenerate", response_model=APIKeyCreateResponse)
|
||||||
async def regenerate_api_key(
|
async def regenerate_api_key(
|
||||||
api_key_id: int,
|
api_key_id: int,
|
||||||
current_user: User = Depends(get_superadmin_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""重新生成 API Key 密钥(secret 仅在此处返回一次)"""
|
"""重新生成 API Key 密钥(secret 仅在此处返回一次,只能操作自己的 Key)"""
|
||||||
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
|
||||||
api_key = result.scalar_one_or_none()
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise HTTPException(status_code=404, detail="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()
|
full_key, key_hash, key_prefix = generate_api_key()
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试
|
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试
|
||||||
|
- 修复对话线程与 Agent 配置错位的问题:发送消息时将当前 `agent_config_id` 绑定到 thread 的 `extra_metadata`,线程列表接口返回该绑定值,前端切换历史 thread 时会自动恢复对应配置
|
||||||
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
||||||
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
||||||
- 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败
|
- 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { apiSuperAdminGet, apiSuperAdminPost, apiSuperAdminPut, apiDelete } from './base'
|
import { apiGet, apiPost, apiPut, apiDelete } from './base'
|
||||||
|
|
||||||
export const apikeyApi = {
|
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}`),
|
delete: (id) => apiDelete(`/api/apikey/${id}`),
|
||||||
|
|
||||||
regenerate: (id) => apiSuperAdminPost(`/api/apikey/${id}/regenerate`)
|
regenerate: (id) => apiPost(`/api/apikey/${id}/regenerate`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 fetchThreads = async (agentId = null) => {
|
||||||
const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId
|
const targetAgentId = props.singleMode ? agentId || currentAgentId.value : agentId
|
||||||
@ -926,6 +950,8 @@ const sendMessage = async ({
|
|||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setThreadAgentConfigId(threadId, selectedAgentConfigId.value)
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
query: text,
|
query: text,
|
||||||
thread_id: threadId,
|
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
|
chatUIStore.isLoadingMessages = true
|
||||||
try {
|
try {
|
||||||
await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId })
|
await fetchThreadMessages({ agentId: targetAgentId, threadId: chatId })
|
||||||
|
|||||||
@ -148,7 +148,7 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 8px;
|
padding: 6px 8px;
|
||||||
// height: 28px;
|
// height: 28px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
class="sider-item"
|
class="sider-item"
|
||||||
:class="{ activesec: activeTab === 'apikey' }"
|
:class="{ activesec: activeTab === 'apikey' }"
|
||||||
@click="activeTab = 'apikey'"
|
@click="activeTab = 'apikey'"
|
||||||
v-if="userStore.isSuperAdmin"
|
v-if="userStore.isLoggedIn"
|
||||||
>
|
>
|
||||||
<KeyIcon class="icon" :size="18" />
|
<KeyIcon class="icon" :size="18" />
|
||||||
<span>API Key</span>
|
<span>API Key</span>
|
||||||
@ -103,7 +103,7 @@
|
|||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: activeTab === 'apikey' }"
|
:class="{ active: activeTab === 'apikey' }"
|
||||||
@click="activeTab = 'apikey'"
|
@click="activeTab = 'apikey'"
|
||||||
v-if="userStore.isSuperAdmin"
|
v-if="userStore.isLoggedIn"
|
||||||
>
|
>
|
||||||
API Key
|
API Key
|
||||||
</div>
|
</div>
|
||||||
@ -128,7 +128,7 @@
|
|||||||
<DepartmentManagementComponent />
|
<DepartmentManagementComponent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="activeTab === 'apikey'" v-if="userStore.isSuperAdmin">
|
<div v-show="activeTab === 'apikey'" v-if="userStore.isLoggedIn">
|
||||||
<ApiKeyManagementComponent />
|
<ApiKeyManagementComponent />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -175,6 +175,8 @@ watch(
|
|||||||
if (newVal) {
|
if (newVal) {
|
||||||
if (userStore.isAdmin) {
|
if (userStore.isAdmin) {
|
||||||
activeTab.value = 'base'
|
activeTab.value = 'base'
|
||||||
|
} else if (userStore.isLogin) {
|
||||||
|
activeTab.value = 'apikey'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user