From 547633bf38201f3bf1860a5757a1b4ba0bc8f4f7 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:29:31 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=94=AE=E4=B8=8D=E5=8C=B9=E9=85=8D=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20-=20=E5=89=8D=E7=AB=AF=E6=9D=83=E9=99=90=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:前端权限列表显示dify:bind:*,但路由实际检查dify:dataset:*和dify:file:* 导致取消勾选后权限控制失效 解决方案: 1. 创建权限映射工具(permission-mapper.ts) - dify:bind:list/create/update/delete → dify:dataset:manage - 自动将数据库权限键映射为实际生效的权限键 2. 修改角色权限管理页面 - 加载角色权限时应用权限键映射 - 渲染权限列表时显示实际生效的权限键 - 保存权限时使用映射后的权限ID 影响范围: - 知识库管理权限(/chat-with-llm/dataset-manager) - 角色权限分配页面(/role-permissions) 验证方式: 取消勾选dify:dataset:manage后,知识库管理接口应返回403 --- app/routes/role-permissions._index.tsx | 25 ++++- app/utils/permission-mapper.ts | 142 +++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 app/utils/permission-mapper.ts diff --git a/app/routes/role-permissions._index.tsx b/app/routes/role-permissions._index.tsx index 9588c6d..96d40a9 100644 --- a/app/routes/role-permissions._index.tsx +++ b/app/routes/role-permissions._index.tsx @@ -949,6 +949,9 @@ export default function RolePermissions() { const handleSelectRole = async (role: RoleInfo) => { setSelectedRole(role); + // 动态导入权限映射工具 + const { mapPermissions, mapPermissionKey, findDbPermissionKeys } = await import('~/utils/permission-mapper'); + // v3.0: 并行加载数据 const [routesResult, rolePermissions, users] = await Promise.all([ getRoleRoutesWithPermissions(role.id), @@ -959,11 +962,14 @@ export default function RolePermissions() { const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult; // 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions + // 并应用权限键映射(dify:bind:* -> dify:dataset:*, dify:file:*) const permMap = new Map(); const extractPermissions = (routes: RouteInfo[]) => { routes.forEach(route => { if (route.permissions && route.permissions.length > 0) { - permMap.set(route.id, route.permissions); + // 应用权限键映射 + const mappedPermissions = mapPermissions(route.permissions); + permMap.set(route.id, mappedPermissions); } if (route.children) { extractPermissions(route.children); @@ -973,11 +979,24 @@ export default function RolePermissions() { extractPermissions(routesWithPerms); // 从 getRolePermissions 结果中提取已分配的权限ID - const assignedPermissionIds = rolePermissions.map(p => p.permission_id); + // 需要将数据库权限键映射为实际权限键进行对比 + const assignedPermissionIds = rolePermissions.map(p => { + // 查找该权限对应的所有数据库权限键 + const dbKeys = findDbPermissionKeys(p.permission_key); + // 如果使用映射,找到对应的权限ID + const mappedKey = mapPermissionKey(p.permission_key); + + // 在 permMap 中查找对应权限的ID + for (const [routeId, permissions] of permMap.entries()) { + const foundPerm = permissions.find(perm => perm.permission_key === mappedKey); + if (foundPerm) return foundPerm.id; + } + return p.permission_id; + }); setRoutePermissionsMap(permMap); setSelectedRouteIds(routeIds); - setSelectedPermissionIds(assignedPermissionIds); // 使用实际已分配的权限ID + setSelectedPermissionIds(assignedPermissionIds); // 使用映射后的权限ID setExpandedRouteIds([]); // 重置展开状态 setRoleUsers(users); }; diff --git a/app/utils/permission-mapper.ts b/app/utils/permission-mapper.ts new file mode 100644 index 0000000..ea4ad8b --- /dev/null +++ b/app/utils/permission-mapper.ts @@ -0,0 +1,142 @@ +/** + * 权限键映射工具 + * 用于将数据库中的权限键映射为前端显示的权限键 + * + * 问题背景: + * 数据库中的权限键(dify:bind:*)与路由实际检查的权限键(dify:dataset:*, dify:file:*)不一致 + * 导致前端取消勾选后,后端仍然检查实际权限,权限控制失效 + */ + +/** + * 权限键映射表 + * key: 数据库中的权限键 + * value: 前端显示和实际生效的权限键 + */ +const PERMISSION_KEY_MAP: Record = { + // 知识库绑定相关 - 映射到数据集管理权限 + 'dify:bind:list': 'dify:dataset:manage', + 'dify:bind:create': 'dify:dataset:manage', + 'dify:bind:update': 'dify:dataset:manage', + 'dify:bind:delete': 'dify:dataset:manage', +}; + +/** + * 反向映射表:实际权限键 -> 数据库权限键列表 + * 用于检查某个权限是否被正确配置 + */ +const REVERSE_PERMISSION_MAP: Record = { + 'dify:dataset:manage': [ + 'dify:bind:list', + 'dify:bind:create', + 'dify:bind:update', + 'dify:bind:delete', + ], +}; + +/** + * 将数据库权限键转换为显示权限键 + * @param permissionKey 数据库中的权限键 + * @returns 前端显示的权限键 + * + * @example + * mapPermissionKey('dify:bind:list') // 返回 'dify:dataset:manage' + * mapPermissionKey('dify:file:read') // 返回 'dify:file:read'(无映射) + */ +export function mapPermissionKey(permissionKey: string): string { + return PERMISSION_KEY_MAP[permissionKey] || permissionKey; +} + +/** + * 批量转换权限键列表 + * @param permissionKeys 权限键数组 + * @returns 转换后的权限键数组(去重) + */ +export function mapPermissionKeys(permissionKeys: string[]): string[] { + const mappedKeys = permissionKeys.map(key => mapPermissionKey(key)); + return [...new Set(mappedKeys)]; // 去重 +} + +/** + * 反向查找:根据实际权限键找到对应的数据库权限键列表 + * @param effectivePermissionKey 实际生效的权限键(如 'dify:dataset:manage') + * @returns 对应的数据库权限键列表 + * + * @example + * findDbPermissionKeys('dify:dataset:manage') + * // 返回 ['dify:bind:list', 'dify:bind:create', 'dify:bind:update', 'dify:bind:delete'] + */ +export function findDbPermissionKeys(effectivePermissionKey: string): string[] { + return REVERSE_PERMISSION_MAP[effectivePermissionKey] || [effectivePermissionKey]; +} + +/** + * 转换权限对象:将数据库权限对象转换为显示权限对象 + * @param permission 权限对象(来自数据库) + * @returns 转换后的权限对象 + */ +export interface Permission { + id: number; + permission_key: string; + display_name: string; + api_method?: string; + api_path?: string; + [key: string]: any; +} + +export function mapPermission(permission: Permission): Permission { + const mappedKey = mapPermissionKey(permission.permission_key); + + // 如果权限键被映射,更新显示名称(可选) + let displayName = permission.display_name; + if (mappedKey !== permission.permission_key) { + // 根据映射后的权限键更新显示名称 + if (mappedKey === 'dify:dataset:manage') { + displayName = '知识库管理(查看、创建、编辑、删除)'; + } + } + + return { + ...permission, + permission_key: mappedKey, + display_name: displayName, + }; +} + +/** + * 批量转换权限列表 + * @param permissions 权限对象数组 + * @returns 转换后的权限对象数组(去重) + */ +export function mapPermissions(permissions: Permission[]): Permission[] { + const mappedMap = new Map(); + + permissions.forEach(permission => { + const mapped = mapPermission(permission); + + // 如果映射后的权限键已存在,合并(保留第一个或根据业务逻辑) + if (!mappedMap.has(mapped.permission_key)) { + mappedMap.set(mapped.permission_key, mapped); + } + }); + + return Array.from(mappedMap.values()); +} + +/** + * 检查权限是否受映射影响 + * @param permissionKey 权限键 + * @returns 是否有映射关系 + */ +export function hasPermissionMapping(permissionKey: string): boolean { + return PERMISSION_KEY_MAP[permissionKey] !== undefined; +} + +/** + * 获取权限映射的说明信息 + * @returns 映射关系的说明文本 + */ +export function getPermissionMappingInfo(): string { + return `权限映射说明:\n` + + `- dify:bind:list/create/update/delete → dify:dataset:manage(知识库管理)\n` + + `取消勾选后,对应接口将返回403权限不足错误`; +} From 209f57a5b75c0e73c596f60117a40b2c39bd9a6f Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:32:09 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96403=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA\n\n=E5=BD=93?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B2=A1=E6=9C=89dify:dataset:manage?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=97=B6=EF=BC=8C\n=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0/=E5=88=A0=E9=99=A4=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E8=BF=94=E5=9B=9E403=EF=BC=8C\n=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8F=8B=E5=A5=BD=E7=9A=84"=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=8D=E8=B6=B3"=E6=8F=90=E7=A4=BA=EF=BC=8C\n=E8=80=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E9=80=9A=E7=94=A8=E7=9A=84"=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=A4=B1=E8=B4=A5=EF=BC=8C=E8=AF=B7=E7=A8=8D=E5=90=8E?= =?UTF-8?q?=E9=87=8D=E8=AF=95"\n\n=F0=9F=A4=96=20Generated=20with=20[Claud?= =?UTF-8?q?e=20Code](https://claude.com/claude-code)\n\nCo-Authored-By:=20?= =?UTF-8?q?Claude=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/use-area-dataset-config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/hooks/use-area-dataset-config.ts b/app/hooks/use-area-dataset-config.ts index f964ca1..bb155cf 100644 --- a/app/hooks/use-area-dataset-config.ts +++ b/app/hooks/use-area-dataset-config.ts @@ -255,7 +255,12 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { } } catch (error: any) { console.error('更新知识库绑定失败:', error); - message.error('更新失败,请稍后重试'); + // 检查是否为403权限不足错误 + if (error?.response?.status === 403 || error?.status === 403) { + message.error('权限不足:您没有编辑知识库绑定的权限'); + } else { + message.error('更新失败,请稍后重试'); + } return false; } finally { setSubmitLoading(false); From 34029db395b12c81d3e2e539f7f61b91716e28a0 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:35:34 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/api.v3.dify.area-datasets.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/routes/api.v3.dify.area-datasets.tsx b/app/routes/api.v3.dify.area-datasets.tsx index 74342b8..b3f7315 100644 --- a/app/routes/api.v3.dify.area-datasets.tsx +++ b/app/routes/api.v3.dify.area-datasets.tsx @@ -1,6 +1,10 @@ /** * GET /api/v3/dify/area-datasets - 获取所有知识库绑定列表(管理员) * POST /api/v3/dify/area-datasets - 创建知识库绑定 + * + * 权限说明: + * GET: @require_permission_v2("dify:dataset:manage") + * POST: @require_permission_v2("dify:dataset:manage") */ import { type LoaderFunctionArgs, json } from '@remix-run/node'; From d3418ef31bcbe0416748950b988344bd63c490a3 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:39:39 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E7=9A=84=E5=8F=8D=E5=90=91=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:权限映射只在显示层面进行,但保存时还需要反向映射回数据库权限键 解决方案: 1. 分离显示权限和原始权限: - originalRoutePermissionsMap:存储未映射的原始权限(用于保存) - routePermissionsMap:存储映射后的权限(用于显示) - originalAllPermissions:存储所有原始权限的列表 2. 加载角色权限时: - 从API获取角色已分配的权限ID(原始ID) - 直接存储到 selectedPermissionIds - 不做任何映射转换 3. 显示权限列表时: - 从原始权限构建映射后的权限(合并相同的) - 用户看到的就是映射后的权限(如dify:dataset:manage) - 但勾选状态基于原始权限ID 4. 保存权限时: - 直接使用 selectedPermissionIds(原始ID) - 无需反向映射 验证方式: 1. 取消勾选 dify:dataset:manage → 数据库中4个bind权限被DENY → 接口返回403 2. 重新勾选 dify:dataset:manage → 数据库中4个bind权限被GRANT → 接口可访问 --- app/routes/role-permissions._index.tsx | 64 +++++++++++++++----------- app/utils/permission-mapper.ts | 56 ++++++++++++++++++++++ 2 files changed, 94 insertions(+), 26 deletions(-) diff --git a/app/routes/role-permissions._index.tsx b/app/routes/role-permissions._index.tsx index 96d40a9..add48cc 100644 --- a/app/routes/role-permissions._index.tsx +++ b/app/routes/role-permissions._index.tsx @@ -945,12 +945,17 @@ export default function RolePermissions() { } }; + // ==================== 权限状态管理 ==================== + // 存储原始的、未映射的权限(用于保存时) + const [originalRoutePermissionsMap, setOriginalRoutePermissionsMap] = useState>(new Map()); + const [originalAllPermissions, setOriginalAllPermissions] = useState([]); + // 选择角色 const handleSelectRole = async (role: RoleInfo) => { setSelectedRole(role); // 动态导入权限映射工具 - const { mapPermissions, mapPermissionKey, findDbPermissionKeys } = await import('~/utils/permission-mapper'); + const { mapPermissions } = await import('~/utils/permission-mapper'); // v3.0: 并行加载数据 const [routesResult, rolePermissions, users] = await Promise.all([ @@ -961,42 +966,49 @@ export default function RolePermissions() { const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult; - // 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions - // 并应用权限键映射(dify:bind:* -> dify:dataset:*, dify:file:*) - const permMap = new Map(); - const extractPermissions = (routes: RouteInfo[]) => { + // 构建原始权限映射(未映射的,用于保存) + const originalPermMap = new Map(); + // 存储所有原始权限的列表 + const allOriginalPerms: ApiPermission[] = []; + const extractOriginalPermissions = (routes: RouteInfo[]) => { routes.forEach(route => { if (route.permissions && route.permissions.length > 0) { - // 应用权限键映射 - const mappedPermissions = mapPermissions(route.permissions); - permMap.set(route.id, mappedPermissions); + originalPermMap.set(route.id, route.permissions); + allOriginalPerms.push(...route.permissions); } if (route.children) { - extractPermissions(route.children); + extractOriginalPermissions(route.children); } }); }; - extractPermissions(routesWithPerms); + extractOriginalPermissions(routesWithPerms); - // 从 getRolePermissions 结果中提取已分配的权限ID - // 需要将数据库权限键映射为实际权限键进行对比 - const assignedPermissionIds = rolePermissions.map(p => { - // 查找该权限对应的所有数据库权限键 - const dbKeys = findDbPermissionKeys(p.permission_key); - // 如果使用映射,找到对应的权限ID - const mappedKey = mapPermissionKey(p.permission_key); + // 存储原始权限 + setOriginalRoutePermissionsMap(originalPermMap); + setOriginalAllPermissions(allOriginalPerms); - // 在 permMap 中查找对应权限的ID - for (const [routeId, permissions] of permMap.entries()) { - const foundPerm = permissions.find(perm => perm.permission_key === mappedKey); - if (foundPerm) return foundPerm.id; - } - return p.permission_id; - }); + // 构建映射后的权限映射(用于显示) + const displayPermMap = new Map(); + const extractDisplayPermissions = (routes: RouteInfo[]) => { + routes.forEach(route => { + if (route.permissions && route.permissions.length > 0) { + const mappedPermissions = mapPermissions(route.permissions); + displayPermMap.set(route.id, mappedPermissions); + } + if (route.children) { + extractDisplayPermissions(route.children); + } + }); + }; + extractDisplayPermissions(routesWithPerms); - setRoutePermissionsMap(permMap); + // 从 getRolePermissions 结果中提取已分配的权限ID(原始ID) + const assignedPermissionIds = rolePermissions.map(p => p.permission_id); + + // 存储状态 + setRoutePermissionsMap(displayPermMap); // 用于显示 setSelectedRouteIds(routeIds); - setSelectedPermissionIds(assignedPermissionIds); // 使用映射后的权限ID + setSelectedPermissionIds(assignedPermissionIds); // 使用原始权限ID setExpandedRouteIds([]); // 重置展开状态 setRoleUsers(users); }; diff --git a/app/utils/permission-mapper.ts b/app/utils/permission-mapper.ts index ea4ad8b..fd49873 100644 --- a/app/utils/permission-mapper.ts +++ b/app/utils/permission-mapper.ts @@ -122,6 +122,62 @@ export function mapPermissions(permissions: Permission[]): Permission[] { return Array.from(mappedMap.values()); } +/** + * 反向转换:将显示权限键转换为数据库权限键列表(用于保存权限) + * @param permissionKey 显示权限键(如 'dify:dataset:manage') + * @returns 对应的数据库权限键列表 + * + * @example + * reverseMapPermissionKey('dify:dataset:manage') + * // 返回 ['dify:bind:list', 'dify:bind:create', 'dify:bind:update', 'dify:bind:delete'] + * + * reverseMapPermissionKey('dify:file:read') + * // 返回 ['dify:file:read'](无映射) + */ +export function reverseMapPermissionKey(permissionKey: string): string[] { + // 查找所有映射到该权限键的原始权限键 + const originalKeys: string[] = []; + for (const [key, mappedKey] of Object.entries(PERMISSION_KEY_MAP)) { + if (mappedKey === permissionKey) { + originalKeys.push(key); + } + } + return originalKeys.length > 0 ? originalKeys : [permissionKey]; +} + +/** + * 根据显示的权限ID查找对应的数据库权限ID(用于保存) + * 这是一个复杂操作,因为前端显示的权限ID是经过映射的 + * + * @param displayPermissionId 前端显示的权限ID(映射后的) + * @param allMappedPermissions 所有映射后的权限列表(从路由获取的) + * @param originalPermissions 原始的权限列表(未映射的) + * @returns 对应的数据库权限ID列表 + */ +export function findDbPermissionIds( + displayPermissionId: number, + allMappedPermissions: Permission[], + originalPermissions: Permission[] +): number[] { + // 查找显示权限对应的显示权限对象 + const displayPerm = allMappedPermissions.find(p => p.id === displayPermissionId); + if (!displayPerm) return []; + + // 将显示权限键反向映射为数据库权限键列表 + const dbKeys = reverseMapPermissionKey(displayPerm.permission_key); + + // 在原始权限列表中查找这些数据库权限键对应的ID + const dbIds: number[] = []; + for (const key of dbKeys) { + const originalPerm = originalPermissions.find(p => p.permission_key === key); + if (originalPerm) { + dbIds.push(originalPerm.id); + } + } + + return dbIds; +} + /** * 检查权限是否受映射影响 * @param permissionKey 权限键 From 32fe0da48a4ddbb7b616c93a10541dfb198859ab Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 15:47:26 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4dify:dataset:?= =?UTF-8?q?manage=E6=9D=83=E9=99=90=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据库已经删除了统一的dify:dataset:manage权限(id:103), 保留了细分的CRUD权限: - dify:bind:list (查看) - dify:bind:create (创建) - dify:bind:update (更新) - dify:bind:delete (删除) 修改内容: 1. 从permission-mapper.ts中移除所有权限键映射 2. 现在前端直接显示数据库权限键,不再进行任何转换 3. 权限检查直接使用数据库中的细分权限 优势: - 更灵活的角色权限配置 - 不同角色可以拥有不同的操作组合 - 符合最小权限原则 --- app/utils/permission-mapper.ts | 104 +++++++++------------------------ 1 file changed, 28 insertions(+), 76 deletions(-) diff --git a/app/utils/permission-mapper.ts b/app/utils/permission-mapper.ts index fd49873..7f747b7 100644 --- a/app/utils/permission-mapper.ts +++ b/app/utils/permission-mapper.ts @@ -1,46 +1,36 @@ /** * 权限键映射工具 - * 用于将数据库中的权限键映射为前端显示的权限键 - * - * 问题背景: - * 数据库中的权限键(dify:bind:*)与路由实际检查的权限键(dify:dataset:*, dify:file:*)不一致 - * 导致前端取消勾选后,后端仍然检查实际权限,权限控制失效 + * 用于处理数据库权限键与路由装饰器检查权限键不一致的情况 */ /** * 权限键映射表 * key: 数据库中的权限键 * value: 前端显示和实际生效的权限键 + * + * 当前配置:不做映射,直接显示数据库权限键 + * 因为权限已经细分为CRUD操作,不需要合并显示 */ const PERMISSION_KEY_MAP: Record = { - // 知识库绑定相关 - 映射到数据集管理权限 - 'dify:bind:list': 'dify:dataset:manage', - 'dify:bind:create': 'dify:dataset:manage', - 'dify:bind:update': 'dify:dataset:manage', - 'dify:bind:delete': 'dify:dataset:manage', + // 知识库绑定相关 - 直接显示数据库权限键,不映射 + // 'dify:bind:list': 'dify:bind:list', + // 'dify:bind:create': 'dify:bind:create', + // 'dify:bind:update': 'dify:bind:update', + // 'dify:bind:delete': 'dify:bind:delete', }; /** * 反向映射表:实际权限键 -> 数据库权限键列表 - * 用于检查某个权限是否被正确配置 + * 当前配置:不进行反向映射 */ const REVERSE_PERMISSION_MAP: Record = { - 'dify:dataset:manage': [ - 'dify:bind:list', - 'dify:bind:create', - 'dify:bind:update', - 'dify:bind:delete', - ], + // 'dify:bind:list': ['dify:bind:list'], }; /** * 将数据库权限键转换为显示权限键 * @param permissionKey 数据库中的权限键 - * @returns 前端显示的权限键 - * - * @example - * mapPermissionKey('dify:bind:list') // 返回 'dify:dataset:manage' - * mapPermissionKey('dify:file:read') // 返回 'dify:file:read'(无映射) + * @returns 前端显示的权限键(无映射时返回原值) */ export function mapPermissionKey(permissionKey: string): string { return PERMISSION_KEY_MAP[permissionKey] || permissionKey; @@ -57,22 +47,23 @@ export function mapPermissionKeys(permissionKeys: string[]): string[] { } /** - * 反向查找:根据实际权限键找到对应的数据库权限键列表 - * @param effectivePermissionKey 实际生效的权限键(如 'dify:dataset:manage') - * @returns 对应的数据库权限键列表 - * - * @example - * findDbPermissionKeys('dify:dataset:manage') - * // 返回 ['dify:bind:list', 'dify:bind:create', 'dify:bind:update', 'dify:bind:delete'] + * 反向查找:根据显示权限键找到对应的数据库权限键 + * @param effectivePermissionKey 显示权限键 + * @returns 对应的数据库权限键列表(无映射时返回原值) */ -export function findDbPermissionKeys(effectivePermissionKey: string): string[] { - return REVERSE_PERMISSION_MAP[effectivePermissionKey] || [effectivePermissionKey]; +export function reverseMapPermissionKey(permissionKey: string): string[] { + // 查找所有映射到该权限键的原始权限键 + const originalKeys: string[] = []; + for (const [key, mappedKey] of Object.entries(PERMISSION_KEY_MAP)) { + if (mappedKey === permissionKey) { + originalKeys.push(key); + } + } + return originalKeys.length > 0 ? originalKeys : [permissionKey]; } /** * 转换权限对象:将数据库权限对象转换为显示权限对象 - * @param permission 权限对象(来自数据库) - * @returns 转换后的权限对象 */ export interface Permission { id: number; @@ -86,13 +77,10 @@ export interface Permission { export function mapPermission(permission: Permission): Permission { const mappedKey = mapPermissionKey(permission.permission_key); - // 如果权限键被映射,更新显示名称(可选) + // 如果权限键被映射,更新显示名称(可根据业务逻辑调整) let displayName = permission.display_name; if (mappedKey !== permission.permission_key) { - // 根据映射后的权限键更新显示名称 - if (mappedKey === 'dify:dataset:manage') { - displayName = '知识库管理(查看、创建、编辑、删除)'; - } + // 这里可以添加自定义的显示名称映射逻辑 } return { @@ -113,7 +101,7 @@ export function mapPermissions(permissions: Permission[]): Permission[] { permissions.forEach(permission => { const mapped = mapPermission(permission); - // 如果映射后的权限键已存在,合并(保留第一个或根据业务逻辑) + // 如果映射后的权限键已存在,合并(保留第一个) if (!mappedMap.has(mapped.permission_key)) { mappedMap.set(mapped.permission_key, mapped); } @@ -122,51 +110,19 @@ export function mapPermissions(permissions: Permission[]): Permission[] { return Array.from(mappedMap.values()); } -/** - * 反向转换:将显示权限键转换为数据库权限键列表(用于保存权限) - * @param permissionKey 显示权限键(如 'dify:dataset:manage') - * @returns 对应的数据库权限键列表 - * - * @example - * reverseMapPermissionKey('dify:dataset:manage') - * // 返回 ['dify:bind:list', 'dify:bind:create', 'dify:bind:update', 'dify:bind:delete'] - * - * reverseMapPermissionKey('dify:file:read') - * // 返回 ['dify:file:read'](无映射) - */ -export function reverseMapPermissionKey(permissionKey: string): string[] { - // 查找所有映射到该权限键的原始权限键 - const originalKeys: string[] = []; - for (const [key, mappedKey] of Object.entries(PERMISSION_KEY_MAP)) { - if (mappedKey === permissionKey) { - originalKeys.push(key); - } - } - return originalKeys.length > 0 ? originalKeys : [permissionKey]; -} - /** * 根据显示的权限ID查找对应的数据库权限ID(用于保存) - * 这是一个复杂操作,因为前端显示的权限ID是经过映射的 - * - * @param displayPermissionId 前端显示的权限ID(映射后的) - * @param allMappedPermissions 所有映射后的权限列表(从路由获取的) - * @param originalPermissions 原始的权限列表(未映射的) - * @returns 对应的数据库权限ID列表 */ export function findDbPermissionIds( displayPermissionId: number, allMappedPermissions: Permission[], originalPermissions: Permission[] ): number[] { - // 查找显示权限对应的显示权限对象 const displayPerm = allMappedPermissions.find(p => p.id === displayPermissionId); if (!displayPerm) return []; - // 将显示权限键反向映射为数据库权限键列表 const dbKeys = reverseMapPermissionKey(displayPerm.permission_key); - // 在原始权限列表中查找这些数据库权限键对应的ID const dbIds: number[] = []; for (const key of dbKeys) { const originalPerm = originalPermissions.find(p => p.permission_key === key); @@ -180,8 +136,6 @@ export function findDbPermissionIds( /** * 检查权限是否受映射影响 - * @param permissionKey 权限键 - * @returns 是否有映射关系 */ export function hasPermissionMapping(permissionKey: string): boolean { return PERMISSION_KEY_MAP[permissionKey] !== undefined; @@ -189,10 +143,8 @@ export function hasPermissionMapping(permissionKey: string): boolean { /** * 获取权限映射的说明信息 - * @returns 映射关系的说明文本 */ export function getPermissionMappingInfo(): string { return `权限映射说明:\n` + - `- dify:bind:list/create/update/delete → dify:dataset:manage(知识库管理)\n` + - `取消勾选后,对应接口将返回403权限不足错误`; + `当前未启用权限键映射,直接使用数据库权限键`; } From a4479971a845d31912e34a367ef22a28759e4940 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 8 Dec 2025 16:12:01 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80403=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA=E4=B8=BA'=E6=97=A0=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=93=8D=E4=BD=9C'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 1. handleCreate: 捕获403错误,显示'无权限操作:您没有创建知识库绑定的权限' 2. handleUpdate: 捕获403错误,显示'无权限操作:您没有编辑知识库绑定的权限' 3. handleDelete: 捕获403错误,显示'无权限操作:您没有删除知识库绑定的权限' 检查逻辑: - error?.response?.status === 403 - error?.status === 403 - error?.code === 403 优势:用户能清楚知道是权限问题,而不是系统错误 --- app/hooks/use-area-dataset-config.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/hooks/use-area-dataset-config.ts b/app/hooks/use-area-dataset-config.ts index bb155cf..23b8bfe 100644 --- a/app/hooks/use-area-dataset-config.ts +++ b/app/hooks/use-area-dataset-config.ts @@ -222,7 +222,12 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { } } catch (error: any) { console.error('创建知识库绑定失败:', error); - message.error('创建失败,请稍后重试'); + // 检查是否为403权限不足错误 + if (error?.response?.status === 403 || error?.status === 403 || error?.code === 403) { + message.error('无权限操作:您没有创建知识库绑定的权限'); + } else { + message.error('创建失败,请稍后重试'); + } return false; } finally { setSubmitLoading(false); @@ -256,8 +261,8 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { } catch (error: any) { console.error('更新知识库绑定失败:', error); // 检查是否为403权限不足错误 - if (error?.response?.status === 403 || error?.status === 403) { - message.error('权限不足:您没有编辑知识库绑定的权限'); + if (error?.response?.status === 403 || error?.status === 403 || error?.code === 403) { + message.error('无权限操作:您没有编辑知识库绑定的权限'); } else { message.error('更新失败,请稍后重试'); } @@ -292,7 +297,12 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { } } catch (error: any) { console.error('删除知识库绑定失败:', error); - message.error('删除失败,请稍后重试'); + // 检查是否为403权限不足错误 + if (error?.response?.status === 403 || error?.status === 403 || error?.code === 403) { + message.error('无权限操作:您没有删除知识库绑定的权限'); + } else { + message.error('删除失败,请稍后重试'); + } return false; } },