From 5bee9288b9fb2f4d844cc32461634ff19c7436aa Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Fri, 10 Apr 2026 16:20:32 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=9B=BF=E6=8D=A2=20Dify=20?= =?UTF-8?q?=E4=B8=BA=E8=87=AA=E5=BB=BA=20RAG=E5=8E=BB=E5=AE=9E=E7=8E=B0=20?= =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E4=BA=86=E8=8B=A5=E5=B9=B2?= =?UTF-8?q?=E6=97=A0=E6=9D=83=E9=99=90=E6=97=B6=E7=9A=84=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=AD=202=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=B8=AA=E7=94=9F=E6=88=90=E5=90=8E=E7=BB=AD?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE=E9=97=AE=E9=A2=98=E7=9A=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=203=E3=80=81=E9=87=8D=E6=9E=84=E4=BA=86=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E9=97=AE=E7=AD=94=E9=83=A8=E5=88=86=E7=9A=84=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=204=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E8=8B=A5=E5=B9=B2=E6=B8=B2=E6=9F=93=E4=B8=8D?= =?UTF-8?q?=E6=81=B0=E5=BD=93=E7=9A=84=E6=A0=B7=E5=BC=8F=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + app/api/dify-chat/chat.ts | 5 + app/api/dify-chat/client.ts | 11 +- app/api/dify-chat/types.ts | 1 + app/api/v3/dify/area-datasets.ts | 1 - app/components/dify-chat/chat-message.tsx | 61 +++-- app/components/dify-chat/index.tsx | 93 ++++--- app/components/dify-chat/sidebar.tsx | 18 +- .../area-dataset-config.tsx | 239 +++++++----------- .../dify-dataset-manager/dataset-settings.tsx | 9 +- .../dify-dataset-manager/document-detail.tsx | 64 +++-- .../dify-dataset-manager/document-list.tsx | 62 +++-- app/components/dify-dataset-manager/index.tsx | 8 + .../dify-dataset-manager/layout.tsx | 11 +- app/hooks/dify-chat-apps/useChatApps.ts | 33 ++- .../dify-dataset-manager/document-detail.ts | 19 +- app/hooks/dify-dataset-manager/index.ts | 14 +- app/hooks/use-area-dataset-config.ts | 2 +- app/hooks/use-chat-message.ts | 18 +- app/hooks/usePermission.tsx | 3 + app/root.tsx | 3 + app/routes/api.chat-messages.tsx | 6 +- app/routes/api.conversations.$id.name.tsx | 3 +- app/routes/api.conversations.$id.tsx | 3 +- app/routes/api.conversations.tsx | 8 +- .../api.messages.$messageId.feedbacks.tsx | 3 +- app/routes/api.parameters.tsx | 3 +- .../components/dify-dataset-manager/index.css | 4 +- .../dify-dataset-manager/dataset-settings.ts | 1 + .../dify-dataset-manager/document-detail.ts | 1 + .../dify-dataset-manager/document-list.ts | 2 + 31 files changed, 407 insertions(+), 304 deletions(-) diff --git a/.gitignore b/.gitignore index 4e0782d..dc96605 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ auth_doc/ teach/ typecheck_result.txt *.DS_Store + +CLAUDE.md \ No newline at end of file diff --git a/app/api/dify-chat/chat.ts b/app/api/dify-chat/chat.ts index fb9801b..09508f4 100644 --- a/app/api/dify-chat/chat.ts +++ b/app/api/dify-chat/chat.ts @@ -159,6 +159,11 @@ export const difyClient = { return { result: 'success' }; } catch (error: any) { + // 权限不足等明确错误需要抛出,不能吞掉 + if (error.message?.includes('403') || error.message?.includes('401')) { + throw error; + } + // 网络超时等不确定错误才降级为成功(Dify 可能已执行删除) console.warn('[Dify Chat] 删除会话请求失败,但可能已成功删除:', error.message); return { result: 'success' }; } diff --git a/app/api/dify-chat/client.ts b/app/api/dify-chat/client.ts index 44e3a53..8a968ea 100644 --- a/app/api/dify-chat/client.ts +++ b/app/api/dify-chat/client.ts @@ -241,9 +241,14 @@ export async function deleteConversation(id: string): Promise<{ result: string } * console.log('开场白:', opening_statement); * ``` */ -export async function fetchAppParams(): Promise { - const url = `${API_URL}/parameters`; - console.log('⚙️ [Dify Client] 获取应用参数:', { url }); +export async function fetchAppParams(appId?: string): Promise { + const params = new URLSearchParams(); + if (appId) { + params.append('app_id', appId); + } + + const url = params.toString() ? `${API_URL}/parameters?${params}` : `${API_URL}/parameters`; + console.log('⚙️ [Dify Client] 获取应用参数:', { url, appId }); try { const response = await axios.get(url, { diff --git a/app/api/dify-chat/types.ts b/app/api/dify-chat/types.ts index cba4967..d7debbf 100644 --- a/app/api/dify-chat/types.ts +++ b/app/api/dify-chat/types.ts @@ -365,6 +365,7 @@ export interface MessageEnd { metadata?: { annotation_reply?: any; retriever_resources?: RetrieverResource[]; + suggested_questions?: string[]; usage?: { prompt_tokens: number; completion_tokens: number; diff --git a/app/api/v3/dify/area-datasets.ts b/app/api/v3/dify/area-datasets.ts index 0a10b58..09cec43 100644 --- a/app/api/v3/dify/area-datasets.ts +++ b/app/api/v3/dify/area-datasets.ts @@ -55,7 +55,6 @@ export interface AreasResponse { export interface CreateDatasetRequest { area: string; - dataset_id: string; dataset_name: string; dataset_description?: string; is_default?: boolean; diff --git a/app/components/dify-chat/chat-message.tsx b/app/components/dify-chat/chat-message.tsx index 4c0f6c3..50aea69 100644 --- a/app/components/dify-chat/chat-message.tsx +++ b/app/components/dify-chat/chat-message.tsx @@ -13,6 +13,7 @@ interface ChatMessageProps { onFeedback?: (messageId: string, feedback: Feedbacktype) => void; isResponding?: boolean; onRegenerate?: (messageId: string) => void; + onSuggestedQuestionClick?: (question: string) => void; } /** @@ -22,7 +23,8 @@ export default function ChatMessage({ message, onFeedback, isResponding = false, - onRegenerate + onRegenerate, + onSuggestedQuestionClick, }: ChatMessageProps) { const [feedback, setFeedback] = useState<'like' | 'dislike' | null>( message.feedback?.rating || null @@ -124,28 +126,51 @@ export default function ChatMessage({ }; /** - * 渲染建议问题 + * 渲染建议问题(继续探索) */ const renderSuggestedQuestions = () => { if (!suggestedQuestions || suggestedQuestions.length === 0) return null; return ( -
-
建议问题:
-
+
+
+ + 继续探索 +
+
{suggestedQuestions.map((question, index) => ( - + + {question} + + ))}
@@ -198,10 +223,14 @@ export default function ChatMessage({
{/* 消息内容 */}
- {isAnswer ? renderAnswerContent() : ( + {isAnswer ? ( + <> + {renderAnswerContent()} + {!isResponding && renderSuggestedQuestions()} + + ) : (
- {/* {renderImages(message_files)} */}
)}
diff --git a/app/components/dify-chat/index.tsx b/app/components/dify-chat/index.tsx index ed9d8df..10d0656 100644 --- a/app/components/dify-chat/index.tsx +++ b/app/components/dify-chat/index.tsx @@ -1,5 +1,5 @@ import { useBoolean, useGetState } from 'ahooks'; -import { Layout, theme } from 'antd'; +import { Layout } from 'antd'; import { useEffect, useRef, useState } from 'react'; import ChatInput from './chat-input'; import ChatMessage from './chat-message'; @@ -8,6 +8,7 @@ import ChatSidebar, { type ChatSidebarRef } from './sidebar'; import type { ChatItem, ConversationItem } from '~/api/dify-chat'; import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-chat'; import { CHAT_CONFIG } from '../../config/chat'; +import { usePermission } from '~/hooks/usePermission'; import useChatMessage from '../../hooks/use-chat-message'; import useConversation from '../../hooks/use-conversation'; import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps'; @@ -28,29 +29,34 @@ interface ChatTheme { } /** - * 获取主题token - 避免在SSR环境中调用 + * 主题配置常量 + * 避免 SSR 环境下调用 theme.useToken() 导致 CSS-in-JS 注入报错 */ -function useChatTheme(): ChatTheme { - // Ant Design的theme.useToken()必须在组件顶层调用,不能放在useEffect中 - const antdToken = typeof window !== 'undefined' ? theme.useToken().token : null; - - return { - colorBgContainer: antdToken?.colorBgContainer || '#ffffff', - borderRadiusLG: antdToken?.borderRadiusLG || 8, - }; -} +const CHAT_THEME: ChatTheme = { + colorBgContainer: '#ffffff', + borderRadiusLG: 8, +}; /** * 主聊天组件 * 实现单页面应用模式,参考webapp-conversation的初始化逻辑 */ export default function Chat() { + // SSR 兼容:Ant Design 的 CSS-in-JS 在 Remix SSR 环境下 + // hydration 时会因找不到样式容器而报错,需要客户端渲染 + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + + // 权限检查 + const { hasPermission: checkPerm } = usePermission(); + const canChat = checkPerm('dify:chat:use'); + // 侧边栏状态 const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(false); - // 获取主题配置,避免SSR错误 - const { colorBgContainer, borderRadiusLG } = useChatTheme(); + // 主题配置 + const { colorBgContainer, borderRadiusLG } = CHAT_THEME; // 对话应用管理 const { @@ -162,8 +168,11 @@ export default function Chat() { // 应用状态 const [appUnavailable, setAppUnavailable] = useState(false); const [isUnknownReason, setIsUnknownReason] = useState(false); + const [conversationPermissionDenied, setConversationPermissionDenied] = useState(false); const [inited, setInited] = useState(false); const [promptConfig, setPromptConfig] = useState(null); + // 防止重复初始化 + const [initializing, setInitializing] = useState(false); // 会话状态管理 const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false); @@ -507,7 +516,8 @@ export default function Chat() { }; /** - * 组件初始化 - 参考webapp-conversation的逻辑 + * 组件初始化 - 等待 currentChatApp 就绪后再获取会话列表 + * 确保按当前应用过滤会话,避免不同应用的会话混在一起 */ useEffect(() => { if (!hasSetAppConfig) { @@ -516,14 +526,28 @@ export default function Chat() { return; } + // 必须等 currentChatApp 就绪,否则不知道该获取哪个应用的会话 + if (!currentChatApp || initializing || inited) { + return; + } + + setInitializing(true); + (async () => { try { - // console.log('🚀 开始初始化聊天应用...'); + console.log('🚀 [Chat] 开始初始化,当前应用:', currentChatApp.app_name, currentChatApp.app_id); - // 并行获取会话列表和应用参数 + // 用当前应用的 appId 获取会话列表(失败时降级为空列表,不阻塞初始化) const [conversationData, appParams] = await Promise.all([ - fetchConversations(), - fetchAppParams() + fetchConversations(currentChatApp.app_id).catch(err => { + console.warn('⚠️ [Chat] 获取会话列表失败(权限不足或网络问题),降级为空列表:', err.message); + setConversationPermissionDenied(true); + return { data: [] }; + }), + fetchAppParams().catch(err => { + console.warn('⚠️ [Chat] 获取应用参数失败,使用默认值:', err.message); + return { data: { user_input_form: [], opening_statement: '' } }; + }), ]); console.log('📋 [Chat] 获取到的数据:', { conversationData, appParams }); @@ -532,13 +556,8 @@ export default function Chat() { const conversations = (conversationData as any).data || []; console.log('📋 [Chat] 会话列表:', conversations); - if ((conversationData as any).error) { - console.error('❌ [Chat] 获取会话列表失败:', (conversationData as any).error); - throw new Error((conversationData as any).error); - } - // 处理当前会话ID - const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID); + const _conversationId = getConversationIdFromStorage(currentChatApp.app_id); const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId); console.log('💾 [Chat] 初始化 - 本地存储的会话ID:', { @@ -568,15 +587,15 @@ export default function Chat() { // 如果存在有效的会话ID,则设置为当前会话 if (isNotNewConversation) { console.log('🎯 [Chat] 初始化 - 设置当前会话ID:', _conversationId); - setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false); + setCurrConversationId(_conversationId, currentChatApp.app_id, false); } else { // 如果localStorage为空或会话不存在,自动创建新会话 console.log('🆕 [Chat] 初始化 - localStorage为空或会话不存在,创建新会话'); - setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false); + setCurrConversationId('-1', currentChatApp.app_id, false); } setInited(true); - // console.log('✅ 聊天应用初始化完成'); + console.log('✅ [Chat] 聊天应用初始化完成'); } catch (e: any) { console.error('❌ 初始化失败:', e); if (e.status === 404) { @@ -587,7 +606,7 @@ export default function Chat() { } } })(); - }, []); + }, [currentChatApp]); // 监听会话切换 useEffect(() => { @@ -663,6 +682,18 @@ export default function Chat() { const conversationName = currConversationInfo?.name || '新对话'; const conversationIntroduction = currConversationInfo?.introduction || ''; + // SSR 兼容:等客户端挂载后再渲染 Ant Design 组件 + if (!mounted) { + return ( +
+
+
+

正在加载...

+
+
+ ); + } + return ( {/* 移动端遮罩层 - 点击可收起侧边栏 */} @@ -704,6 +735,7 @@ export default function Chat() { loadingChatApps={loadingChatApps} currentChatApp={currentChatApp} onChatAppChange={handleChatAppChange} + conversationReadOnly={conversationPermissionDenied} /> {/* 主内容区域 */} @@ -738,6 +770,7 @@ export default function Chat() { message={item} isResponding={isResponding && item.id === chatList[chatList.length - 1]?.id} onFeedback={handleFeedback} + onSuggestedQuestionClick={(question) => handleSendMessage(question)} /> ))}
@@ -747,8 +780,8 @@ export default function Chat() {
diff --git a/app/components/dify-chat/sidebar.tsx b/app/components/dify-chat/sidebar.tsx index a3eec13..192cbd9 100644 --- a/app/components/dify-chat/sidebar.tsx +++ b/app/components/dify-chat/sidebar.tsx @@ -13,6 +13,7 @@ import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme, import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'; import type { ChatApp } from '~/api/dify-chat-apps/types'; import type { ConversationItem } from '~/api/dify-chat'; +import { usePermission } from '~/hooks/usePermission'; import { deleteConversation, renameConversation } from '~/api/dify-chat'; import '../../styles/components/chat-with-llm/sidebar.css'; @@ -32,6 +33,8 @@ interface ChatSidebarProps { currentChatApp: ChatApp | null; onChatAppChange: (appId: string) => void; onConversationRenamed?: (conversationId: string, newName: string) => void; + /** 会话列表权限被拒绝时,隐藏重命名/删除按钮 */ + conversationReadOnly?: boolean; } // 暴露给父组件的方法接口 @@ -55,7 +58,10 @@ const ChatSidebar = forwardRef(({ onNewConversation, onConversationDeleted, onConversationRenamed, + conversationReadOnly = false, }, ref) => { + const { hasPermission } = usePermission(); + const canDeleteConversation = hasPermission('dify:conversation:delete'); const [searchValue, setSearchValue] = useState(''); const [renameModalVisible, setRenameModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); @@ -208,24 +214,24 @@ const ChatSidebar = forwardRef(({ {conv.name} - {!collapsed && ( + {!collapsed && (!conversationReadOnly || canDeleteConversation) && ( , label: '重命名', onClick: () => handleRename(conv), - }, - { + }] : []), + ...(canDeleteConversation ? [{ key: 'delete', icon: , label: '删除', danger: true, onClick: () => handleDeleteClick(conv), - }, - ], + }] : []), + ].filter(Boolean), }} trigger={['click']} placement="bottomRight" diff --git a/app/components/dify-dataset-manager/area-dataset-config.tsx b/app/components/dify-dataset-manager/area-dataset-config.tsx index 0ecf43f..96aaa64 100644 --- a/app/components/dify-dataset-manager/area-dataset-config.tsx +++ b/app/components/dify-dataset-manager/area-dataset-config.tsx @@ -7,7 +7,7 @@ * @version 1.0.0 */ -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { Card, Table, @@ -24,7 +24,6 @@ import { Flex, Typography, Popconfirm, - Spin, Tooltip, } from 'antd'; import { @@ -35,8 +34,7 @@ import { CheckCircleOutlined, } from '@ant-design/icons'; import { useAreaDatasetConfig } from '~/hooks/use-area-dataset-config'; -import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi'; -import type { Dataset as DifyDataset } from '~/api/dify-dataset/type'; +import { usePermission } from '~/hooks/usePermission'; import type { AreaDataset } from '~/api/v3/dify/area-datasets'; const { Title, Text } = Typography; @@ -90,13 +88,11 @@ export default function AreaDatasetConfig() { canManageDataset, } = useAreaDatasetConfig(); + const { userRole: rawUserRole, userArea: rawUserArea } = usePermission(); + const isProvincialAdmin = rawUserRole === 'provincial_admin'; + // 内部状态 const [form] = Form.useForm(); - const [difyDatasets, setDifyDatasets] = useState([]); - const [difyDatasetsLoading, setDifyDatasetsLoading] = useState(false); - const [difyDatasetsTotal, setDifyDatasetsTotal] = useState(0); - const [difyDatasetsPage, setDifyDatasetsPage] = useState(1); - const [isLoadingDifyDatasets, setIsLoadingDifyDatasets] = useState(false); // ==================== Effects ==================== @@ -107,7 +103,6 @@ export default function AreaDatasetConfig() { if (record) { form.setFieldsValue({ area: record.area, - dataset_id: record.dataset_id, dataset_name: record.dataset_name, dataset_description: record.dataset_description, is_public: record.is_public, @@ -116,51 +111,14 @@ export default function AreaDatasetConfig() { }); } } else if (!editingId && modalVisible) { - // 新增时重置表单 form.resetFields(); - loadDifyDatasets(); // 加载Dify知识库列表 + // 非省级管理员自动填充地区 + if (!isProvincialAdmin && rawUserArea) { + form.setFieldValue('area', rawUserArea); + } } }, [editingId, modalVisible, datasets, form]); - // ==================== Dify Datasets Loading ==================== - - /** - * 从Dify API加载知识库列表 - */ - const loadDifyDatasets = async (pageNum: number = 1) => { - if (isLoadingDifyDatasets) return; - - setIsLoadingDifyDatasets(true); - try { - const response = await fetchDatasets(pageNum, 20); - setDifyDatasets(response.data); - setDifyDatasetsTotal(response.total); - setDifyDatasetsPage(pageNum); - setDifyDatasetsLoading(false); - } catch (error: any) { - console.error('加载Dify知识库列表失败:', error); - message.error('加载Dify知识库列表失败'); - setDifyDatasetsLoading(false); - } finally { - setIsLoadingDifyDatasets(false); - } - }; - - /** - * Dify数据集选择器滚动加载 - */ - const handleDatasetSelectScroll = (e: React.UIEvent) => { - const { target } = e; - const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement; - - // 滚动到底部且还有更多数据时加载下一页 - if (scrollHeight - scrollTop === clientHeight && - difyDatasets.length < difyDatasetsTotal && - !difyDatasetsLoading) { - loadDifyDatasets(difyDatasetsPage + 1); - } - }; - // ==================== Event Handlers ==================== /** @@ -168,13 +126,12 @@ export default function AreaDatasetConfig() { */ const handleCreateClick = () => { if (!canManageDataset) { - message.error('您没有创建知识库绑定的权限'); + message.error('您没有创建知识库的权限'); return; } setEditingId(null); setModalVisible(true); form.resetFields(); - loadDifyDatasets(); }; /** @@ -208,6 +165,24 @@ export default function AreaDatasetConfig() { * 处理表单提交 */ const handleFormSubmit = async (values: any) => { + // 编辑时检查 is_default 是否从 false 变为 true + if (editingId && values.is_default) { + const record = datasets.find((item) => item.id === editingId); + if (record && !record.is_default) { + Modal.confirm({ + title: '切换默认知识库', + content: '确认将此知识库设为默认?该地区的对话助手将自动绑定此知识库进行问答。', + okText: '确认', + cancelText: '取消', + onOk: () => doSubmit(values), + }); + return; + } + } + await doSubmit(values); + }; + + const doSubmit = async (values: any) => { let success = false; if (editingId) { @@ -269,6 +244,7 @@ export default function AreaDatasetConfig() { key: 'dataset_name', width: 200, ellipsis: true, + align: 'center', render: (text: string) => ( @@ -277,25 +253,26 @@ export default function AreaDatasetConfig() { ), }, - { - title: '知识库ID', - dataIndex: 'dataset_id', - key: 'dataset_id', - width: 200, - ellipsis: true, - render: (text: string) => ( - - - {text.substring(0, 8)}...{text.substring(text.length - 4)} - - - ), - }, + // { + // title: '知识库ID', + // dataIndex: 'dataset_id', + // key: 'dataset_id', + // width: 200, + // ellipsis: true, + // render: (text: string) => ( + // + // + // {text.substring(0, 8)}...{text.substring(text.length - 4)} + // + // + // ), + // }, { title: '描述', dataIndex: 'dataset_description', key: 'dataset_description', ellipsis: true, + align: 'center', render: (text: string) => text ? ( @@ -319,7 +296,7 @@ export default function AreaDatasetConfig() { )} {record.is_default && ( - + 默认 )} @@ -330,7 +307,7 @@ export default function AreaDatasetConfig() { title: '排序', dataIndex: 'sort_order', key: 'sort_order', - width: 70, + width: 170, align: 'center' as const, sorter: (a: AreaDataset, b: AreaDataset) => a.sort_order - b.sort_order, }, @@ -364,29 +341,37 @@ export default function AreaDatasetConfig() { key: 'actions', width: 120, fixed: 'right' as const, - render: (_: any, record: AreaDataset) => ( - - - handleDeleteClick(record.id)} - okText="确定" - cancelText="取消" - > - - - - ), + render: (_: any, record: AreaDataset) => { + // 市级管理员只能编辑自己地区的知识库 + const canEdit = isProvincialAdmin || record.area === rawUserArea; + return ( + + {canEdit && ( + + )} + {isProvincialAdmin && ( + handleDeleteClick(record.id)} + okText="确定" + cancelText="取消" + > + + + )} + + ); + }, }, ] : []), @@ -432,7 +417,7 @@ export default function AreaDatasetConfig() { icon={} onClick={handleCreateClick} > - 新增绑定 + 新增知识库 )} @@ -503,7 +488,7 @@ export default function AreaDatasetConfig() { {/* 新增/编辑对话框 */} form.submit()} onCancel={handleFormCancel} @@ -529,55 +514,12 @@ export default function AreaDatasetConfig() { > ( -
- {menu} - {difyDatasets.length < difyDatasetsTotal && ( -
- - - 加载中... - -
- )} -
- )} - onDropdownVisibleChange={(open) => { - if (open && !editingId) { - loadDifyDatasets(); - } - }} - options={difyDatasets.map((ds) => ({ - label: ( - - {ds.name} - - ID: {ds.id} - - - ), - value: ds.id, - }))} - styles={{ popup: { root: { maxHeight: '300px' } } }} + options={ + isProvincialAdmin + ? (Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : []) + : (rawUserArea ? [{ label: rawUserArea, value: rawUserArea }] : []) + } /> @@ -594,7 +536,7 @@ export default function AreaDatasetConfig() { {/* 知识库描述 */} - {/* @@ -603,36 +545,33 @@ export default function AreaDatasetConfig() { rows={3} maxLength={500} /> - */} + - {/* 高级设置折叠面板 */} + {/* 高级设置 */}
高级设置 - {/* 是否公开 */} - {/* 是否默认 */} - {/* 排序顺序 */} - diff --git a/app/components/dify-dataset-manager/document-detail.tsx b/app/components/dify-dataset-manager/document-detail.tsx index a087490..2ea2c8e 100644 --- a/app/components/dify-dataset-manager/document-detail.tsx +++ b/app/components/dify-dataset-manager/document-detail.tsx @@ -16,6 +16,7 @@ import { EyeOutlined, } from '@ant-design/icons'; import { useDocumentDetail } from '~/hooks/dify-dataset-manager/document-detail'; +import { usePermission } from '~/hooks/usePermission'; import type { DocumentDetailProps } from '~/types/dify-dataset-manager/document-detail'; import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-detail'; @@ -26,6 +27,7 @@ import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-de export default function DocumentDetail({ datasetId, document, + canEditDataset = true, }: DocumentDetailProps) { const { settings, @@ -41,6 +43,10 @@ export default function DocumentDetail({ handleSaveAndProcess, } = useDocumentDetail(datasetId, document); + const { hasPermission } = usePermission(); + const canManageDoc = hasPermission('dify:document:manage') && canEditDataset; + const readOnly = !canManageDoc || isProcessing; + if (!document) { return (
@@ -83,7 +89,7 @@ export default function DocumentDetail({ value={settings.separator} onChange={(e) => updateSettings('separator', e.target.value)} placeholder="\n\n" - disabled={isProcessing} + disabled={readOnly} className="setting-input" />
@@ -102,7 +108,7 @@ export default function DocumentDetail({ onChange={(value) => updateSettings('maxTokens', value || 500)} min={100} max={4000} - disabled={isProcessing} + disabled={readOnly} className="setting-input-number" /> characters @@ -123,7 +129,7 @@ export default function DocumentDetail({ onChange={(value) => updateSettings('chunkOverlap', value || 50)} min={0} max={500} - disabled={isProcessing} + disabled={readOnly} className="setting-input-number" /> characters @@ -141,7 +147,7 @@ export default function DocumentDetail({ updateSettings('removeExtraSpaces', e.target.checked)} - disabled={isProcessing} + disabled={readOnly} > 替换掉连续的空格、换行符和制表符 @@ -149,7 +155,7 @@ export default function DocumentDetail({ updateSettings('removeUrlsEmails', e.target.checked)} - disabled={isProcessing} + disabled={readOnly} > 删除所有 URL 和电子邮件地址 @@ -163,16 +169,16 @@ export default function DocumentDetail({

索引方式

!isProcessing && updateSettings('indexingTechnique', 'high_quality')} + className={`index-option ${settings.indexingTechnique === 'high_quality' ? 'active' : ''} ${readOnly ? 'disabled' : ''}`} + onClick={() => !readOnly && updateSettings('indexingTechnique', 'high_quality')} > 高质量 推荐
!isProcessing && updateSettings('indexingTechnique', 'economy')} + className={`index-option ${settings.indexingTechnique === 'economy' ? 'active' : ''} ${readOnly ? 'disabled' : ''}`} + onClick={() => !readOnly && updateSettings('indexingTechnique', 'economy')} > 经济 @@ -190,29 +196,33 @@ export default function DocumentDetail({ > 预览块 - + {canManageDoc && ( + + )}
{/* 保存并处理按钮 */} -
- -
+ {canManageDoc && ( +
+ +
+ )}
{/* 右侧预览区域 */} diff --git a/app/components/dify-dataset-manager/document-list.tsx b/app/components/dify-dataset-manager/document-list.tsx index 8ba10e4..c26109d 100644 --- a/app/components/dify-dataset-manager/document-list.tsx +++ b/app/components/dify-dataset-manager/document-list.tsx @@ -22,6 +22,7 @@ import type { ColumnsType } from 'antd/es/table'; import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes'; import { useDocumentList } from '~/hooks/dify-dataset-manager/document-list'; import type { DocumentListProps } from '~/types/dify-dataset-manager/document-list'; +import { usePermission } from '~/hooks/usePermission'; import '../../styles/components/dify-dataset-manager/index.css'; import DocumentUpload from './document-upload'; @@ -40,6 +41,7 @@ export default function DocumentList({ onDocumentStatusChanged, onRefresh, onViewDocument, + canEditDataset = true, }: DocumentListProps) { const { searchValue, @@ -57,6 +59,9 @@ export default function DocumentList({ filterDocuments, } = useDocumentList(datasetId, onDocumentDeleted, onDocumentStatusChanged, onRefresh); + const { hasPermission } = usePermission(); + const canWrite = hasPermission('dify:document:manage') && canEditDataset; + // 过滤文档 const filteredDocuments = filterDocuments(documents); @@ -111,6 +116,7 @@ export default function DocumentList({ handleToggleStatus(record.id, checked)} /> ), @@ -136,24 +142,26 @@ export default function DocumentList({ onClick={() => onViewDocument?.(record)} /> - handleDelete(record.id)} - okText="确定" - cancelText="取消" - okButtonProps={{ danger: true }} - > - - + {canWrite && ( + + )}
diff --git a/app/components/dify-dataset-manager/index.tsx b/app/components/dify-dataset-manager/index.tsx index 26eb1e1..ae54cc6 100644 --- a/app/components/dify-dataset-manager/index.tsx +++ b/app/components/dify-dataset-manager/index.tsx @@ -6,6 +6,7 @@ import RetrieveTest from './retrieve-test'; import DatasetSettings from './dataset-settings'; import AreaDatasetConfig from './area-dataset-config'; import { useDatasetManager } from '~/hooks/dify-dataset-manager'; +import { usePermission } from '~/hooks/usePermission'; import '../../styles/components/dify-dataset-manager/index.css'; /** @@ -43,6 +44,10 @@ export default function DatasetManager() { handleDatasetChange, } = useDatasetManager(); + // 判断当前用户是否能编辑当前知识库(省级管理员可编辑全部,市级管理员只能编辑本地区) + const { userRole, userArea } = usePermission(); + const canEditDataset = userRole === 'provincial_admin' || ((dataset as any)?.area === userArea); + // 加载中状态 if (!inited || loadingDataset) { return ( @@ -80,6 +85,7 @@ export default function DatasetManager() { ); } @@ -98,6 +104,7 @@ export default function DatasetManager() { onDocumentStatusChanged={handleDocumentStatusChanged} onRefresh={handleRefresh} onViewDocument={handleViewDocument} + canEditDataset={canEditDataset} /> ); } @@ -118,6 +125,7 @@ export default function DatasetManager() { ); } diff --git a/app/components/dify-dataset-manager/layout.tsx b/app/components/dify-dataset-manager/layout.tsx index 64483e1..14a8620 100644 --- a/app/components/dify-dataset-manager/layout.tsx +++ b/app/components/dify-dataset-manager/layout.tsx @@ -9,6 +9,7 @@ import { SwapOutlined, } from '@ant-design/icons'; import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout'; +import { usePermission } from '~/hooks/usePermission'; /** * 知识库布局组件 @@ -25,11 +26,13 @@ export default function DatasetLayout({ loadingAvailableDatasets = false, onDatasetChange, }: DatasetLayoutProps) { + const { hasPermission } = usePermission(); + const menuItems: MenuItem[] = [ - { key: 'documents', icon: , label: '文档' }, - { key: 'retrieve', icon: , label: '召回测试' }, - { key: 'area-config', icon: , label: '配置管理' }, - { key: 'settings', icon: , label: '设置' }, + ...(hasPermission('dify:dataset:read') ? [{ key: 'documents' as MenuTab, icon: , label: '文档' }] : []), + ...(hasPermission('dify:retrieve:test') ? [{ key: 'retrieve' as MenuTab, icon: , label: '召回测试' }] : []), + ...(hasPermission('dify:config:manage') ? [{ key: 'area-config' as MenuTab, icon: , label: '配置管理' }] : []), + ...(hasPermission('dify:settings:write') ? [{ key: 'settings' as MenuTab, icon: , label: '设置' }] : []), ]; // 是否显示知识库选择器(有多个知识库时显示) diff --git a/app/hooks/dify-chat-apps/useChatApps.ts b/app/hooks/dify-chat-apps/useChatApps.ts index 821e8be..963dc91 100644 --- a/app/hooks/dify-chat-apps/useChatApps.ts +++ b/app/hooks/dify-chat-apps/useChatApps.ts @@ -136,36 +136,33 @@ export function useChatApps() { setLoadingDefault(true); setError(null); + let resolved = false; // 用局部变量跟踪,避免 React state 异步读取的问题 + try { try { - console.log('[useChatApps] ==================== 开始初始化对话应用 ===================='); - // 尝试加载可用应用列表 - console.log('[useChatApps] 步骤1: 调用loadChatApps()加载我的应用列表...'); const apps = await loadChatApps(); - console.log('[useChatApps] 步骤1完成: 加载到', apps.length, '个应用'); - if (apps.length > 0) { - // 查找默认应用 const defaultApp = apps.find((item) => item.is_default) || apps[0]; - // console.log('[useChatApps] 默认对话应用:', apps); setCurrentChatApp(defaultApp); - console.log('[useChatApps] ==================== 初始化完成(路径1) ===================='); + resolved = true; } else { - // 如果没有配置应用,尝试获取默认应用 - console.log('[useChatApps] 应用列表为空,调用loadDefaultChatApp()...'); - await loadDefaultChatApp(); - console.log('[useChatApps] ==================== 初始化完成(路径2) ===================='); + const app = await loadDefaultChatApp(); + if (app) resolved = true; } } catch (err) { - // 加载应用列表失败,尝试获取默认应用 - console.warn('[useChatApps] 加载应用列表失败,尝试获取默认应用:', err); - await loadDefaultChatApp(); - console.log('[useChatApps] ==================== 初始化完成(路径3) ===================='); + const app = await loadDefaultChatApp(); + if (app) resolved = true; } } catch (err: any) { - console.error('[useChatApps] 初始化失败:', err); - setError(err.message || '加载对话应用失败'); + console.warn('[useChatApps] 初始化异常:', err.message); } finally { + if (!resolved) { + // 权限不足等情况,构造占位应用让页面能渲染(输入框会被禁用) + const fallbackApp = { app_id: '_fallback', app_name: '法务问答', description: '', is_default: true } as any; + setChatApps([fallbackApp]); + setCurrentChatApp(fallbackApp); + setError(null); + } setLoadingChatApps(false); setLoadingDefault(false); setInited(true); diff --git a/app/hooks/dify-dataset-manager/document-detail.ts b/app/hooks/dify-dataset-manager/document-detail.ts index c349ce7..93ba5ca 100644 --- a/app/hooks/dify-dataset-manager/document-detail.ts +++ b/app/hooks/dify-dataset-manager/document-detail.ts @@ -94,11 +94,24 @@ export function useDocumentDetail(datasetId: string, document: Document | null) pollIndexingStatus(batch); }, [stopPolling, pollIndexingStatus]); - // 当文档变化时重置设置 + // 当文档变化时,从文档已有的 process_rule 回显设置,无则使用默认值 useEffect(() => { if (document) { - // 可以从文档中读取已有的设置,这里使用默认值 - setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS); + const rule = (document as any).process_rule; + if (rule?.mode === 'custom' && rule?.rules) { + const seg = rule.rules.segmentation || {}; + const preRules = rule.rules.pre_processing_rules || []; + setSettings({ + separator: (seg.separator || '\\n\\n').replace(/\n/g, '\\n'), + maxTokens: seg.max_tokens || DEFAULT_DOCUMENT_DETAIL_SETTINGS.maxTokens, + chunkOverlap: DEFAULT_DOCUMENT_DETAIL_SETTINGS.chunkOverlap, + removeExtraSpaces: preRules.find((r: any) => r.id === 'remove_extra_spaces')?.enabled ?? DEFAULT_DOCUMENT_DETAIL_SETTINGS.removeExtraSpaces, + removeUrlsEmails: preRules.find((r: any) => r.id === 'remove_urls_emails')?.enabled ?? DEFAULT_DOCUMENT_DETAIL_SETTINGS.removeUrlsEmails, + indexingTechnique: DEFAULT_DOCUMENT_DETAIL_SETTINGS.indexingTechnique, + }); + } else { + setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS); + } setPreviewSegments([]); setShowPreview(false); setIsProcessing(false); diff --git a/app/hooks/dify-dataset-manager/index.ts b/app/hooks/dify-dataset-manager/index.ts index 547a4d6..6c4b59c 100644 --- a/app/hooks/dify-dataset-manager/index.ts +++ b/app/hooks/dify-dataset-manager/index.ts @@ -105,8 +105,10 @@ export function useDatasetManager() { await loadDocuments(datasetId, 1); } catch (err: any) { console.error('[DatasetManager] 加载知识库详情失败:', err); - setError(err.message || '加载知识库失败'); - message.error('加载知识库失败'); + const is403 = err.message?.includes('403') || err.response?.status === 403; + const msg = is403 ? '您没有查看知识库的权限' : (err.message || '加载知识库失败'); + setError(msg); + message.error(msg); } finally { setLoadingDataset(false); } @@ -151,13 +153,15 @@ export function useDatasetManager() { setDataset(fullDataset); await loadDocuments(firstDatasetId, 1); } else { - setError('未找到知识库,请先在Dify中创建知识库'); + setError('未找到知识库,请先联系管理员创建知识库'); } } } catch (err: any) { console.error('[DatasetManager] 加载知识库失败:', err); - setError(err.message || '加载知识库失败'); - message.error('加载知识库失败'); + const is403 = err.message?.includes('403') || err.response?.status === 403; + const msg = is403 ? '您没有查看知识库的权限' : (err.message || '加载知识库失败'); + setError(msg); + message.error(msg); } finally { setLoadingDataset(false); setInited(true); diff --git a/app/hooks/use-area-dataset-config.ts b/app/hooks/use-area-dataset-config.ts index f0d27e1..0ef471d 100644 --- a/app/hooks/use-area-dataset-config.ts +++ b/app/hooks/use-area-dataset-config.ts @@ -72,7 +72,7 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { // 根据权限判断是否可以管理知识库配置 // 权限键:dify:bind:update(知识库绑定更新权限) // 降级方案:如果 permissionMap 中没有配置权限,usePermission 会自动降级为角色判断 - const canManageDataset = hasPermission('dify:bind:update'); + const canManageDataset = hasPermission('dify:config:manage'); const canViewDataset = true; // 所有登录用户都可以查看 // 🔍 调试日志(修复后可删除) diff --git a/app/hooks/use-chat-message.ts b/app/hooks/use-chat-message.ts index b74ae50..072d135 100644 --- a/app/hooks/use-chat-message.ts +++ b/app/hooks/use-chat-message.ts @@ -426,11 +426,21 @@ export default function useChatMessage({ resourceCount: messageEnd.metadata?.retriever_resources?.length || 0 }); + let needUpdate = false; + // 如果有检索资源,更新响应项 if (messageEnd.metadata?.retriever_resources && messageEnd.metadata.retriever_resources.length > 0) { responseItem.retriever_resources = messageEnd.metadata.retriever_resources; + needUpdate = true; + } - // 更新聊天列表 + // 如果有建议问题,更新响应项 + if (messageEnd.metadata?.suggested_questions && messageEnd.metadata.suggested_questions.length > 0) { + responseItem.suggestedQuestions = messageEnd.metadata.suggested_questions; + needUpdate = true; + } + + if (needUpdate) { updateCurrentQA({ responseItem: { ...responseItem }, questionId, @@ -504,8 +514,12 @@ export default function useChatMessage({ draft[messageIndex].feedback = feedback; } })); - } catch (err) { + } catch (err: any) { logError(`提交反馈时出错: ${err}`); + const msg = err?.message || '提交反馈失败'; + const isPermission = msg.includes('403') || msg.includes('权限'); + const { message: antMessage } = await import('antd'); + antMessage.error(isPermission ? '您没有反馈权限' : msg); } }, [logError, getChatList, setChatList]); diff --git a/app/hooks/usePermission.tsx b/app/hooks/usePermission.tsx index dc482bb..98d56b9 100644 --- a/app/hooks/usePermission.tsx +++ b/app/hooks/usePermission.tsx @@ -28,6 +28,7 @@ interface RootLoaderData { permissions?: string[]; permissionMap?: Record; // ✅ 新增:权限映射表 userRole: string; + userArea?: string; userInfo?: { role_id?: number; role_key?: string; @@ -70,6 +71,7 @@ export function usePermission() { // 从root loader获取权限映射表 const permissionMap = rootData?.permissionMap || {}; const userRole = rootData?.userRole || 'common'; + const userArea = rootData?.userArea || ''; // 🔑 根据当前路由获取权限列表 const currentPath = location.pathname; @@ -245,6 +247,7 @@ export function usePermission() { permissions: currentPermissions, // ✅ 返回当前路由的权限 permissionMap, // ✅ 返回完整的权限映射表 userRole, + userArea, // 基础检查方法 hasPermission, diff --git a/app/root.tsx b/app/root.tsx index 864ed38..829fd64 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -200,6 +200,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 获取用户角色和 JWT(从 Cookie Session) let userRole: UserRole = 'common'; // 默认为普通用户 + let userArea: string = ''; let frontendJWT: string | null = null; let allowedPaths: string[] = []; // 用户允许访问的路由列表 let permissionMap: Record = {}; // ✅ 权限映射表 @@ -209,6 +210,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const { getUserSession } = await import("~/api/login/auth.server"); const session = await getUserSession(request); userRole = session.userRole; + userArea = session.userInfo?.area || ''; frontendJWT = session.frontendJWT || null; // 🔑 检查用户角色和JWT是否为空 @@ -344,6 +346,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 向组件传递路径信息 return Response.json({ userRole, // ✅ 返回真实的用户角色 + userArea, // ✅ 返回用户所属地区 pathname, frontendJWT, isPublicPath, // 传递给客户端,用于判断是否需要认证 diff --git a/app/routes/api.chat-messages.tsx b/app/routes/api.chat-messages.tsx index 8f1705d..2ad8fb6 100644 --- a/app/routes/api.chat-messages.tsx +++ b/app/routes/api.chat-messages.tsx @@ -47,7 +47,8 @@ export async function loader({ request }: LoaderFunctionArgs) { } catch (error: any) { console.error('❌ [API] Chat Messages GET - Error:', error.message); - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return new Response( JSON.stringify({ error: error.message || 'Failed to get messages' }), { @@ -148,7 +149,8 @@ export async function action({ request }: ActionFunctionArgs) { }); // 检查是否是JWT认证失败 - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return new Response( JSON.stringify({ error: error.message || 'Failed to send message' }), diff --git a/app/routes/api.conversations.$id.name.tsx b/app/routes/api.conversations.$id.name.tsx index 54786ed..b475349 100644 --- a/app/routes/api.conversations.$id.name.tsx +++ b/app/routes/api.conversations.$id.name.tsx @@ -50,7 +50,8 @@ export async function action({ request, params }: ActionFunctionArgs) { console.error('❌ [API] Rename Conversation API - Error:', error); // 检查是否是JWT认证失败 - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return json( { diff --git a/app/routes/api.conversations.$id.tsx b/app/routes/api.conversations.$id.tsx index 18cfaf0..8e6920f 100644 --- a/app/routes/api.conversations.$id.tsx +++ b/app/routes/api.conversations.$id.tsx @@ -47,7 +47,8 @@ export async function action({ request, params }: ActionFunctionArgs) { console.error('❌ [API] Delete Conversation API - Error:', error); // 检查是否是JWT认证失败 - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return json( { diff --git a/app/routes/api.conversations.tsx b/app/routes/api.conversations.tsx index d0dc0a4..23c4cd8 100644 --- a/app/routes/api.conversations.tsx +++ b/app/routes/api.conversations.tsx @@ -39,8 +39,12 @@ export async function loader({ request }: LoaderFunctionArgs) { } catch (error: any) { console.error('❌ [API] Conversations API - Error:', error); - // 检查是否是JWT认证失败 - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + // 从错误中提取原始 HTTP 状态码 + const statusMatch = error.message?.match(/(\d{3})/); + const originalStatus = statusMatch ? parseInt(statusMatch[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 + : originalStatus >= 400 && originalStatus < 500 ? originalStatus + : 500; return json( { diff --git a/app/routes/api.messages.$messageId.feedbacks.tsx b/app/routes/api.messages.$messageId.feedbacks.tsx index 09ff907..048ec3a 100644 --- a/app/routes/api.messages.$messageId.feedbacks.tsx +++ b/app/routes/api.messages.$messageId.feedbacks.tsx @@ -55,7 +55,8 @@ export async function action({ request, params }: ActionFunctionArgs) { } catch (error: any) { console.error('[API] Message Feedback - Error:', error.message); - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return new Response( JSON.stringify({ error: error.message || 'Failed to submit feedback' }), { diff --git a/app/routes/api.parameters.tsx b/app/routes/api.parameters.tsx index bc4252c..588d1d0 100644 --- a/app/routes/api.parameters.tsx +++ b/app/routes/api.parameters.tsx @@ -34,7 +34,8 @@ export async function loader({ request }: LoaderFunctionArgs) { console.error('❌ [API] Parameters API - Error:', error); // 检查是否是JWT认证失败 - const status = error.message?.includes('JWT认证失败') ? 401 : 500; + const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0; + const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500; return json( { error: error.message || 'Failed to fetch parameters' }, diff --git a/app/styles/components/dify-dataset-manager/index.css b/app/styles/components/dify-dataset-manager/index.css index c3339c9..f35bcb8 100644 --- a/app/styles/components/dify-dataset-manager/index.css +++ b/app/styles/components/dify-dataset-manager/index.css @@ -855,11 +855,11 @@ display: flex; flex-direction: column; gap: 12px; - /* 关键修复:添加高度约束和内部滚动 */ - height: 100%; + flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; + padding-bottom: 24px; } .segment-item { diff --git a/app/types/dify-dataset-manager/dataset-settings.ts b/app/types/dify-dataset-manager/dataset-settings.ts index 94489a7..54fe73c 100644 --- a/app/types/dify-dataset-manager/dataset-settings.ts +++ b/app/types/dify-dataset-manager/dataset-settings.ts @@ -6,6 +6,7 @@ import type { Dataset } from '~/api/dify-dataset/type/datasetTypes'; export interface DatasetSettingsProps { dataset: Dataset | null; onDatasetUpdated: (dataset: Dataset) => void; + canEditDataset?: boolean; } /** diff --git a/app/types/dify-dataset-manager/document-detail.ts b/app/types/dify-dataset-manager/document-detail.ts index e894c96..b1ead7e 100644 --- a/app/types/dify-dataset-manager/document-detail.ts +++ b/app/types/dify-dataset-manager/document-detail.ts @@ -7,6 +7,7 @@ import type { Document } from '~/api/dify-dataset/type/documentTypes'; export interface DocumentDetailProps { datasetId: string; document: Document | null; + canEditDataset?: boolean; } /** diff --git a/app/types/dify-dataset-manager/document-list.ts b/app/types/dify-dataset-manager/document-list.ts index 9bea4c2..da002ff 100644 --- a/app/types/dify-dataset-manager/document-list.ts +++ b/app/types/dify-dataset-manager/document-list.ts @@ -16,6 +16,8 @@ export interface DocumentListProps { onDocumentStatusChanged: (documentId: string, enabled: boolean) => void; onRefresh: () => void; onViewDocument?: (document: Document) => void; + /** 是否能编辑当前知识库(地区归属校验)*/ + canEditDataset?: boolean; } /**