7e6424e9ac
1. 权限检查优化
- 使用 hasPermission('dify:bind:update') 替代硬编码的角色判断
- 支持细粒度的权限控制,市级管理员可以通过授权获得编辑权限
- 保留降级方案,provincial_admin 角色自动拥有所有权限
2. 错误处理优化
- 新增统一的 handleApiError 错误处理函数
- 优先显示后端返回的具体错误信息(error.response.data.msg)
- 支持多种错误格式的提取(axios、fetch、自定义格式)
- 简化 handleCreate、handleUpdate、handleDelete 的错误处理代码
3. 调试支持
- 添加权限检查的调试日志,便于排查问题
- 输出当前路由、用户角色、权限列表等关键信息
修复问题:
- 市级管理员被授予 dify:bind:update 权限后,编辑按钮仍不显示
- 403 错误只显示通用提示,无法看到后端返回的具体错误原因
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
395 lines
11 KiB
TypeScript
395 lines
11 KiB
TypeScript
/**
|
|
* 知识库配置管理 Hook
|
|
*
|
|
* 提供地区-知识库绑定管理功能
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import {
|
|
getMyDatasets,
|
|
getAllDatasets,
|
|
getAvailableAreas,
|
|
createDatasetBinding,
|
|
updateDatasetBinding,
|
|
deleteDatasetBinding,
|
|
type AreaDataset,
|
|
type CreateDatasetRequest,
|
|
type UpdateDatasetRequest,
|
|
} from '~/api/v3/dify/area-datasets';
|
|
import { message } from 'antd';
|
|
import { usePermission } from '~/hooks/usePermission';
|
|
|
|
// ==================== Type Definitions ====================
|
|
|
|
export interface UseAreaDatasetConfigReturn {
|
|
// 数据
|
|
datasets: AreaDataset[];
|
|
loading: boolean;
|
|
total: number;
|
|
userArea: string;
|
|
userRole: string;
|
|
areas: string[];
|
|
areasLoading: boolean;
|
|
|
|
// 筛选 - 支持多选
|
|
filterAreas: string[];
|
|
setFilterAreas: (areas: string[]) => void;
|
|
page: number;
|
|
setPage: (page: number) => void;
|
|
pageSize: number;
|
|
|
|
// 表单状态
|
|
modalVisible: boolean;
|
|
setModalVisible: (visible: boolean) => void;
|
|
editingId: number | null;
|
|
setEditingId: (id: number | null) => void;
|
|
submitLoading: boolean;
|
|
|
|
// 操作方法
|
|
loadDatasets: () => Promise<void>;
|
|
loadAreas: () => Promise<void>;
|
|
handleCreate: (data: CreateDatasetRequest) => Promise<boolean>;
|
|
handleUpdate: (id: number, data: UpdateDatasetRequest) => Promise<boolean>;
|
|
handleDelete: (id: number) => Promise<boolean>;
|
|
|
|
// 权限
|
|
canManageDataset: boolean;
|
|
}
|
|
|
|
// 角色名称映射
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
common: '普通用户',
|
|
admin: '市级管理员',
|
|
provincial_admin: '省级管理员',
|
|
};
|
|
|
|
// ==================== Hook Implementation ====================
|
|
|
|
export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
|
// 权限控制
|
|
const { hasPermission, userRole: permissionUserRole, permissions, permissionMap } = usePermission();
|
|
|
|
// 根据权限判断是否可以管理知识库配置
|
|
// 权限键:dify:bind:update(知识库绑定更新权限)
|
|
// 降级方案:如果 permissionMap 中没有配置权限,usePermission 会自动降级为角色判断
|
|
const canManageDataset = hasPermission('dify:bind:update');
|
|
const canViewDataset = true; // 所有登录用户都可以查看
|
|
|
|
// 🔍 调试日志(修复后可删除)
|
|
if (typeof window !== 'undefined') {
|
|
console.log('[DatasetConfig] 权限检查:', {
|
|
currentPath: window.location.pathname,
|
|
userRole: permissionUserRole,
|
|
hasDifyBindUpdate: hasPermission('dify:bind:update'),
|
|
currentPermissions: permissions,
|
|
canManageDataset,
|
|
});
|
|
}
|
|
|
|
// ==================== 错误处理工具函数 ====================
|
|
|
|
/**
|
|
* 统一处理 API 错误
|
|
* @param error 错误对象
|
|
* @param operation 操作名称(创建/更新/删除)
|
|
*/
|
|
const handleApiError = (error: any, operation: string) => {
|
|
console.error(`${operation}知识库绑定失败:`, error);
|
|
|
|
// 提取错误信息的优先级:
|
|
// 1. error.response.data.msg (axios 响应格式)
|
|
// 2. error.data.msg (其他格式)
|
|
// 3. error.msg (直接错误对象)
|
|
// 4. error.response.data.message (备用字段)
|
|
// 5. error.message (标准错误对象)
|
|
const errorMsg = error?.response?.data?.msg ||
|
|
error?.data?.msg ||
|
|
error?.msg ||
|
|
error?.response?.data?.message ||
|
|
error?.message;
|
|
|
|
// 检查是否为403权限不足错误
|
|
const is403 = error?.response?.status === 403 ||
|
|
error?.status === 403 ||
|
|
error?.code === 403;
|
|
|
|
if (is403) {
|
|
// 403 错误:显示具体的权限错误信息
|
|
message.error(errorMsg || `无权限操作:您没有${operation}知识库绑定的权限`);
|
|
} else {
|
|
// 其他错误:显示具体的错误信息或通用提示
|
|
message.error(errorMsg || `${operation}失败,请稍后重试`);
|
|
}
|
|
};
|
|
|
|
// ==================== 数据状态 ====================
|
|
|
|
// 数据状态
|
|
const [datasets, setDatasets] = useState<AreaDataset[]>([]);
|
|
const [allDatasets, setAllDatasets] = useState<AreaDataset[]>([]); // 保存所有数据用于提取地区
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [total, setTotal] = useState<number>(0);
|
|
const [userArea, setUserArea] = useState<string>('');
|
|
const [apiAreas, setApiAreas] = useState<string[]>([]); // API 返回的地区列表
|
|
const [areasLoading, setAreasLoading] = useState<boolean>(false);
|
|
|
|
// 筛选状态 - 支持多选
|
|
const [filterAreas, setFilterAreas] = useState<string[]>([]);
|
|
const [page, setPage] = useState<number>(1);
|
|
const pageSize = 20;
|
|
|
|
// 表单状态
|
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
|
|
|
|
// 从数据集中提取地区列表
|
|
const extractedAreas = useMemo(() => {
|
|
const areaSet = new Set<string>();
|
|
allDatasets.forEach(ds => {
|
|
if (ds.area) {
|
|
areaSet.add(ds.area);
|
|
}
|
|
});
|
|
return Array.from(areaSet).sort();
|
|
}, [allDatasets]);
|
|
|
|
// 合并 API 返回的地区和从数据中提取的地区
|
|
const areas = useMemo(() => {
|
|
const areaSet = new Set<string>([...apiAreas, ...extractedAreas]);
|
|
return Array.from(areaSet).sort();
|
|
}, [apiAreas, extractedAreas]);
|
|
|
|
// 获取角色显示名称
|
|
const userRoleLabel = ROLE_LABELS[permissionUserRole] || permissionUserRole || '未知角色';
|
|
|
|
// ==================== Data Loading ====================
|
|
|
|
/**
|
|
* 加载知识库列表
|
|
*/
|
|
const loadDatasets = useCallback(async () => {
|
|
if (!canViewDataset) {
|
|
message.warning('您没有查看知识库的权限');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
let response;
|
|
|
|
if (canManageDataset) {
|
|
// 省级管理员:获取所有知识库
|
|
// 如果有多个地区筛选,用逗号分隔传递
|
|
const areaFilter = filterAreas.length > 0 ? filterAreas.join(',') : undefined;
|
|
response = await getAllDatasets({
|
|
area: areaFilter,
|
|
only_enabled: false, // 管理员可以看到所有状态
|
|
page,
|
|
page_size: pageSize,
|
|
});
|
|
} else {
|
|
// 普通用户/市级管理员:获取我的知识库
|
|
response = await getMyDatasets();
|
|
}
|
|
|
|
console.log('[AreaDatasetConfig] API响应:', response);
|
|
|
|
if (response && response.code === 0 && response.data) {
|
|
const dataList = Array.isArray(response.data.data) ? response.data.data : [];
|
|
setDatasets(dataList);
|
|
setTotal(response.data.total || dataList.length);
|
|
|
|
// 如果没有筛选,保存所有数据用于提取地区
|
|
if (filterAreas.length === 0) {
|
|
setAllDatasets(dataList);
|
|
}
|
|
|
|
// 如果是 my 接口,保存用户信息
|
|
if ('user_area' in response.data) {
|
|
setUserArea((response.data as any).user_area || '');
|
|
}
|
|
} else {
|
|
console.error('[AreaDatasetConfig] API响应格式错误:', response);
|
|
message.error(`加载失败: ${response?.message || '未知错误'}`);
|
|
}
|
|
} catch (error: any) {
|
|
console.error('加载知识库失败:', error);
|
|
message.error('加载知识库失败,请稍后重试');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [canManageDataset, canViewDataset, filterAreas, page, pageSize]);
|
|
|
|
/**
|
|
* 加载地区列表(仅省级管理员)
|
|
*/
|
|
const loadAreas = useCallback(async () => {
|
|
if (!canManageDataset) return;
|
|
|
|
setAreasLoading(true);
|
|
try {
|
|
const areasList = await getAvailableAreas();
|
|
console.log('[AreaDatasetConfig] 地区列表响应:', areasList);
|
|
if (Array.isArray(areasList)) {
|
|
setApiAreas(areasList);
|
|
} else {
|
|
console.warn('[AreaDatasetConfig] 地区列表不是数组:', areasList);
|
|
setApiAreas([]);
|
|
}
|
|
} catch (error: any) {
|
|
console.error('加载地区列表失败:', error);
|
|
// 不显示错误提示,因为可以从数据中提取地区
|
|
} finally {
|
|
setAreasLoading(false);
|
|
}
|
|
}, [canManageDataset]);
|
|
|
|
// ==================== Operations ====================
|
|
|
|
/**
|
|
* 创建知识库绑定
|
|
*/
|
|
const handleCreate = useCallback(
|
|
async (data: CreateDatasetRequest): Promise<boolean> => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有创建知识库绑定的权限');
|
|
return false;
|
|
}
|
|
|
|
setSubmitLoading(true);
|
|
try {
|
|
const response = await createDatasetBinding(data);
|
|
|
|
if (response.code === 0) {
|
|
message.success('创建成功');
|
|
await loadDatasets();
|
|
return true;
|
|
} else {
|
|
message.error(`创建失败: ${response.message}`);
|
|
return false;
|
|
}
|
|
} catch (error: any) {
|
|
handleApiError(error, '创建');
|
|
return false;
|
|
} finally {
|
|
setSubmitLoading(false);
|
|
}
|
|
},
|
|
[canManageDataset, loadDatasets]
|
|
);
|
|
|
|
/**
|
|
* 更新知识库绑定
|
|
*/
|
|
const handleUpdate = useCallback(
|
|
async (id: number, data: UpdateDatasetRequest): Promise<boolean> => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有编辑知识库绑定的权限');
|
|
return false;
|
|
}
|
|
|
|
setSubmitLoading(true);
|
|
try {
|
|
const response = await updateDatasetBinding(id, data);
|
|
|
|
if (response.code === 0) {
|
|
message.success('更新成功');
|
|
await loadDatasets();
|
|
return true;
|
|
} else {
|
|
message.error(`更新失败: ${response.message}`);
|
|
return false;
|
|
}
|
|
} catch (error: any) {
|
|
handleApiError(error, '更新');
|
|
return false;
|
|
} finally {
|
|
setSubmitLoading(false);
|
|
}
|
|
},
|
|
[canManageDataset, loadDatasets]
|
|
);
|
|
|
|
/**
|
|
* 删除知识库绑定
|
|
*/
|
|
const handleDelete = useCallback(
|
|
async (id: number): Promise<boolean> => {
|
|
if (!canManageDataset) {
|
|
message.error('您没有删除知识库绑定的权限');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await deleteDatasetBinding(id);
|
|
|
|
if (response.code === 0) {
|
|
message.success('删除成功');
|
|
await loadDatasets();
|
|
return true;
|
|
} else {
|
|
message.error(`删除失败: ${response.message}`);
|
|
return false;
|
|
}
|
|
} catch (error: any) {
|
|
handleApiError(error, '删除');
|
|
return false;
|
|
}
|
|
},
|
|
[canManageDataset, loadDatasets]
|
|
);
|
|
|
|
// ==================== Effects ====================
|
|
|
|
// 初始加载数据
|
|
useEffect(() => {
|
|
loadDatasets();
|
|
}, []); // 只在挂载时加载一次
|
|
|
|
// 加载地区列表
|
|
useEffect(() => {
|
|
loadAreas();
|
|
}, [loadAreas]);
|
|
|
|
// 监听筛选条件和页码变化
|
|
useEffect(() => {
|
|
loadDatasets();
|
|
}, [filterAreas, page]);
|
|
|
|
return {
|
|
// 数据
|
|
datasets,
|
|
loading,
|
|
total,
|
|
userArea,
|
|
userRole: userRoleLabel,
|
|
areas,
|
|
areasLoading,
|
|
|
|
// 筛选
|
|
filterAreas,
|
|
setFilterAreas,
|
|
page,
|
|
setPage,
|
|
pageSize,
|
|
|
|
// 表单状态
|
|
modalVisible,
|
|
setModalVisible,
|
|
editingId,
|
|
setEditingId,
|
|
submitLoading,
|
|
|
|
// 操作方法
|
|
loadDatasets,
|
|
loadAreas,
|
|
handleCreate,
|
|
handleUpdate,
|
|
handleDelete,
|
|
|
|
// 权限
|
|
canManageDataset,
|
|
};
|
|
}
|