diff --git a/src/backend/bisheng/api/v1/workstation.py b/src/backend/bisheng/api/v1/workstation.py index c0f960c95..6fd82e067 100644 --- a/src/backend/bisheng/api/v1/workstation.py +++ b/src/backend/bisheng/api/v1/workstation.py @@ -401,10 +401,13 @@ async def chat_completions( extra['prompt'] = prompt message.extra = json.dumps(extra, ensure_ascii=False) ChatMessageDao.insert_one(message) + except MessageException as e: + error = True + final_res = str(e) except Exception as e: logger.exception(f'Error in processing the prompt') error = True - final_res = 'Error in processing the prompt' + final_res = f'Error in processing the prompt: {str(e)[-100:]}' if not error: messages = WorkStationService.get_chat_history(conversationId, 8)[:-1] diff --git a/src/backend/bisheng/chat/client.py b/src/backend/bisheng/chat/client.py index 4eee8ef2c..a20c4ba5a 100644 --- a/src/backend/bisheng/chat/client.py +++ b/src/backend/bisheng/chat/client.py @@ -2,8 +2,6 @@ import json from queue import Queue from typing import Dict, Callable, List -from bisheng.utils import generate_uuid -from bisheng_langchain.gpts.message_types import LiberalToolMessage from fastapi import WebSocket, status, Request from langchain_core.messages import AIMessage, HumanMessage, BaseMessage, ToolMessage from loguru import logger @@ -20,7 +18,9 @@ from bisheng.database.models.flow import FlowType from bisheng.database.models.message import ChatMessageDao, ChatMessage as ChatMessageModel from bisheng.database.models.session import MessageSession, MessageSessionDao from bisheng.settings import settings +from bisheng.utils import generate_uuid from bisheng.utils.threadpool import thread_pool +from bisheng_langchain.gpts.message_types import LiberalToolMessage class ChatClient: @@ -301,16 +301,7 @@ class ChatClient: _ = await self.add_message('bot', one.json(), 'tool_result') else: logger.warning("unexpected message type") - # for one in result: - # if isinstance(one, AIMessage): - # answer += one.content - # todo: 后续优化代码解释器的实现方案,保证输出的文件可以公开访问 ugly solve - # 获取minio的share地址,把share域名去掉, 为毕昇的部署方案特殊处理下 - for one in self.gpts_agent.tools: - if one.name == "bisheng_code_interpreter": - minio_share = settings.get_minio_conf().sharepoint - answer = answer.replace(f"http://{minio_share}", "") answer_end_type = 'end' # 如果是流式的llm则用end_cover结束, 覆盖之前流式的输出 if getattr(self.gpts_agent.llm, 'streaming', False): diff --git a/src/backend/bisheng/interface/embeddings/custom.py b/src/backend/bisheng/interface/embeddings/custom.py index 00d224204..a6ebd362f 100644 --- a/src/backend/bisheng/interface/embeddings/custom.py +++ b/src/backend/bisheng/interface/embeddings/custom.py @@ -134,6 +134,11 @@ class BishengEmbedding(BaseModel, Embeddings): params.update({ 'model': model_info.model_name, }) + + # 非openai官方但是符合openai接口标准的embedding模型,强制chunk_size=1,防止batch_size太大导致服务报错 + if self.llm_node_type.get(server_info.type) == "OpenAIEmbeddings": + params['chunk_size'] = params.pop('chunk_size', 1) + if server_info.type == LLMServerType.QWEN.value: params = { 'dashscope_api_key': params.get('openai_api_key'), @@ -150,9 +155,6 @@ class BishengEmbedding(BaseModel, Embeddings): LLMServerType.VLLM.value ]: params['openai_api_key'] = params.pop('openai_api_key', None) or 'EMPTY' - params['chunk_size'] = params.pop('chunk_size', 1) - elif server_info.type == LLMServerType.OPENAI.value: - params['chunk_size'] = params.pop('chunk_size', 1) elif server_info.type == LLMServerType.OLLAMA.value: params['query_instruction'] = 'passage: ' return params diff --git a/src/backend/bisheng/interface/llms/custom_ollama.py b/src/backend/bisheng/interface/llms/custom_ollama.py index 580ff67ee..49e94bd5d 100644 --- a/src/backend/bisheng/interface/llms/custom_ollama.py +++ b/src/backend/bisheng/interface/llms/custom_ollama.py @@ -117,67 +117,6 @@ class CustomChatOllamaWithReasoning(ChatOllama): ) yield chunk - def _generate( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - **kwargs - ) -> Any: - """Handle non-streaming calls, extract think content to reasoning_content field.""" - result = super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs) - self._process_generations(result.generations, is_async=False) - return result - - async def _agenerate( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, - **kwargs - ) -> ChatResult: - """Handle async non-streaming calls, extract think content to reasoning_content field.""" - result = await super()._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs) - self._process_generations(result.generations, is_async=True) - return result - - def _stream( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - **kwargs - ) -> Iterator[ChatGenerationChunk]: - """Process streaming output, modifying chunks to add reasoning_content field.""" - logger.debug("CustomChatOllamaWithReasoning._stream called") - self._reset_think_state() - - try: - for chunk in super()._stream(messages, stop=stop, run_manager=run_manager, **kwargs): - yield from self._process_stream_chunk(chunk) - except Exception as e: - logger.error(f"Error in CustomChatOllamaWithReasoning stream processing: {e}") - raise - - async def _astream( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, - **kwargs - ) -> AsyncIterator[ChatGenerationChunk]: - """Process async streaming output, modifying chunks to add reasoning_content field.""" - logger.debug("CustomChatOllamaWithReasoning._astream called") - self._reset_think_state() - - try: - async for chunk in super()._astream(messages, stop=stop, run_manager=run_manager, **kwargs): - for processed_chunk in self._process_stream_chunk(chunk): - yield processed_chunk - except Exception as e: - logger.error(f"Error in CustomChatOllamaWithReasoning async stream processing: {e}") - raise - # Helper methods for better code organization def _reset_think_state(self) -> None: @@ -243,238 +182,3 @@ class CustomChatOllamaWithReasoning(ChatOllama): ), generation_info=generation_info, ) - - def _process_generations(self, generations: List, is_async: bool = False) -> None: - """Process generations to extract think content.""" - if not generations: - return - - async_prefix = "Async " if is_async else "" - - for generation in generations: - try: - self._process_generation_message(generation, async_prefix) - self._process_generation_text(generation, async_prefix) - except Exception as e: - logger.error(f"{async_prefix}Error processing generation: {e}") - - def _process_generation_message(self, generation, async_prefix: str) -> None: - """Process generation message content.""" - if not (hasattr(generation, 'message') and - hasattr(generation.message, 'content')): - return - - original_content = generation.message.content - reasoning_content, cleaned_content = self._extract_think_content(original_content) - - generation.message.content = cleaned_content - - if not hasattr(generation.message, 'additional_kwargs'): - generation.message.additional_kwargs = {} - generation.message.additional_kwargs['reasoning_content'] = reasoning_content or None - - logger.debug( - f"{async_prefix}Original content length: {len(original_content)}, " - f"reasoning length: {len(reasoning_content)}, " - f"processed length: {len(cleaned_content)}" - ) - - def _process_generation_text(self, generation, async_prefix: str) -> None: - """Process generation text content.""" - if not hasattr(generation, 'text'): - return - - original_text = generation.text - reasoning_content, cleaned_text = self._extract_think_content(original_text) - generation.text = cleaned_text - - logger.debug( - f"{async_prefix}Original text length: {len(original_text)}, " - f"processed length: {len(cleaned_text)}" - ) - - def _extract_think_content(self, content: str) -> Tuple[str, str]: - """ - Extract think tag content and return (reasoning_content, cleaned_content). - - Args: - content: Original content with potential think tags - - Returns: - Tuple of (reasoning_content, cleaned_content) - """ - if not content: - return "", "" - - # Extract content inside think tags - think_matches = self.THINK_PATTERN.findall(content) - reasoning_content = '\n'.join(think_matches).strip() if think_matches else '' - - # Remove entire ... blocks - cleaned_content = self.THINK_REMOVAL_PATTERN.sub('', content).strip() - - return reasoning_content, cleaned_content - - def _process_stream_chunk(self, chunk: ChatGenerationChunk) -> Iterator[ChatGenerationChunk]: - """Process a single stream chunk and yield modified chunks.""" - if not (hasattr(chunk, 'message') and hasattr(chunk.message, 'content')): - # Add reasoning_content field for chunks without content - self._ensure_reasoning_content_field(chunk) - yield chunk - return - - content = chunk.message.content or "" - - if not content: - self._ensure_reasoning_content_field(chunk) - yield chunk - return - - logger.debug(f"Processing chunk content: '{content}', in_think state: {self.in_think}") - - # Process content with think tags - modified_chunks = self._process_chunk_with_think_tags(chunk, content) - - # Yield all modified chunks - yield from modified_chunks - - def _ensure_reasoning_content_field(self, chunk: ChatGenerationChunk) -> None: - """Ensure chunk has reasoning_content field set to None.""" - if not hasattr(chunk, 'message') or not chunk.message: - return - - if not hasattr(chunk.message, 'additional_kwargs'): - chunk.message.additional_kwargs = {} - chunk.message.additional_kwargs['reasoning_content'] = None - - def _process_chunk_with_think_tags( - self, - original_chunk: ChatGenerationChunk, - content: str - ) -> List[ChatGenerationChunk]: - """ - Process chunk containing think tags and return list of modified chunks. - - Args: - original_chunk: Original chunk from parent stream - content: Text content of the chunk - - Returns: - List of modified chunks with proper content/reasoning_content fields - """ - chunks = [] - current_pos = 0 - - while current_pos < len(content): - if not self.in_think: - current_pos = self._process_outside_think_tags( - chunks, original_chunk, content, current_pos - ) - else: - current_pos = self._process_inside_think_tags( - chunks, original_chunk, content, current_pos - ) - - if current_pos < 0: # Break condition - break - - return chunks - - def _process_outside_think_tags( - self, - chunks: List[ChatGenerationChunk], - original_chunk: ChatGenerationChunk, - content: str, - current_pos: int - ) -> int: - """Process content when outside think tags.""" - think_start = content.find(self.THINK_START_TAG, current_pos) - - if think_start != -1: - # Found tag, output content before it - before_think = content[current_pos:think_start] - if before_think: - chunk = self._create_content_chunk(original_chunk, before_think) - chunks.append(chunk) - - # Enter think state, skip tag and optional newline - new_pos = think_start + len(self.THINK_START_TAG) - if new_pos < len(content) and content[new_pos] == '\n': - new_pos += 1 - - self.in_think = True - return new_pos - else: - # No tag found, output remaining content - remaining = content[current_pos:] - if remaining: - chunk = self._create_content_chunk(original_chunk, remaining) - chunks.append(chunk) - return -1 # Break condition - - def _process_inside_think_tags( - self, - chunks: List[ChatGenerationChunk], - original_chunk: ChatGenerationChunk, - content: str, - current_pos: int - ) -> int: - """Process content when inside think tags.""" - think_end = content.find(self.THINK_END_TAG, current_pos) - - if think_end != -1: - # Found tag, output reasoning content - reasoning = content[current_pos:think_end] - if reasoning: - chunk = self._create_reasoning_chunk(original_chunk, reasoning) - chunks.append(chunk) - - # Exit think state, skip tag and optional newline - new_pos = think_end + len(self.THINK_END_TAG) - if new_pos < len(content) and content[new_pos] == '\n': - new_pos += 1 - - self.in_think = False - return new_pos - else: - # No tag found, output remaining reasoning content - remaining_reasoning = content[current_pos:] - if remaining_reasoning: - chunk = self._create_reasoning_chunk(original_chunk, remaining_reasoning) - chunks.append(chunk) - return -1 # Break condition - - def _create_content_chunk( - self, - original_chunk: ChatGenerationChunk, - content: str - ) -> ChatGenerationChunk: - """Create a chunk for regular content.""" - return ChatGenerationChunk( - message=AIMessageChunk( - content=content, - additional_kwargs={'reasoning_content': None} - ), - generation_info=original_chunk.generation_info - ) - - def _create_reasoning_chunk( - self, - original_chunk: ChatGenerationChunk, - reasoning_content: str - ) -> ChatGenerationChunk: - """Create a chunk for reasoning content.""" - return ChatGenerationChunk( - message=AIMessageChunk( - content="", - additional_kwargs={'reasoning_content': reasoning_content} - ), - generation_info=original_chunk.generation_info - ) - - # Deprecated method kept for backward compatibility - def _remove_think_tags(self, content: str) -> str: - """Remove think tags, keeping only content outside tags. (Deprecated)""" - logger.warning("_remove_think_tags is deprecated, use _extract_think_content instead") - _, cleaned_content = self._extract_think_content(content) - return cleaned_content diff --git a/src/backend/bisheng/settings.py b/src/backend/bisheng/settings.py index 003f43bee..51784a264 100644 --- a/src/backend/bisheng/settings.py +++ b/src/backend/bisheng/settings.py @@ -78,6 +78,8 @@ class MinioConf(BaseModel): cert_check: Optional[bool] = Field(default=False, description="是否校验证书") endpoint: Optional[str] = Field(default="127.0.0.1:9000", description="minio 地址") sharepoint: Optional[str] = Field(default="127.0.0.1:9000", description="minio 公开访问地址") + share_schema: Optional[bool] = Field(default=False, description="minio 公开访问地址是否使用https") + share_cert_check: Optional[bool] = Field(default=False, description="minio 公开访问地址是否校验证书") access_key: Optional[str] = Field(default="minioadmin", description="minio 用户名") secret_key: Optional[str] = Field(default="minioadmin", description="minio 密码") public_bucket: Optional[str] = Field(default="bisheng", diff --git a/src/backend/bisheng/utils/minio_client.py b/src/backend/bisheng/utils/minio_client.py index 5438a1765..064c0fe52 100644 --- a/src/backend/bisheng/utils/minio_client.py +++ b/src/backend/bisheng/utils/minio_client.py @@ -32,8 +32,8 @@ class MinioClient: endpoint=_MinioConf.sharepoint, access_key=_MinioConf.access_key, secret_key=_MinioConf.secret_key, - secure=_MinioConf.schema, - cert_check=_MinioConf.cert_check) + secure=_MinioConf.share_schema, + cert_check=_MinioConf.share_cert_check) self._init_bucket_conf() @@ -165,7 +165,7 @@ class MinioClient: 获取minio share host """ minio_share = _MinioConf.sharepoint - if _MinioConf.schema: + if _MinioConf.share_schema: return f'https://{minio_share}' return f'http://{minio_share}' diff --git a/src/backend/bisheng_langchain/gpts/tools/code_interpreter/base_executor.py b/src/backend/bisheng_langchain/gpts/tools/code_interpreter/base_executor.py index fd407a802..bb0325c8b 100644 --- a/src/backend/bisheng_langchain/gpts/tools/code_interpreter/base_executor.py +++ b/src/backend/bisheng_langchain/gpts/tools/code_interpreter/base_executor.py @@ -32,8 +32,8 @@ class BaseExecutor(ABC): endpoint=self.minio.get('sharepoint'), access_key=self.minio.get('access_key'), secret_key=self.minio.get('secret_key'), - secure=self.minio.get('schema'), - cert_check=self.minio.get('cert_check'), + secure=self.minio.get('share_schema', False), + cert_check=self.minio.get('share_cert_check', False), ) bucket = self.minio.get('tmp_bucket', 'tmp-dir') logger.debug( @@ -54,4 +54,4 @@ class BaseExecutor(ABC): ) def close(self) -> None: - pass \ No newline at end of file + pass diff --git a/src/frontend/client/src/components/Chat/Input/ChatFormTools.tsx b/src/frontend/client/src/components/Chat/Input/ChatFormTools.tsx index 1355626f9..c7bdba8cb 100644 --- a/src/frontend/client/src/components/Chat/Input/ChatFormTools.tsx +++ b/src/frontend/client/src/components/Chat/Input/ChatFormTools.tsx @@ -88,7 +88,6 @@ export const ChatToolDown = ({ linsi, tools, setTools, config, searchType, setSe const LinsiTools = ({ tools, setTools }) => { const { data: bsConfig } = useGetBsConfig() - const [building] = useModelBuilding() useEffect(() => { const defaultTools = [{ @@ -101,7 +100,7 @@ const LinsiTools = ({ tools, setTools }) => { id: 'knowledge', name: '个人知识库', icon: , - checked: !building + checked: true },] if (bsConfig) { const tools = bsConfig.linsightConfig?.tools || [] @@ -115,7 +114,7 @@ const LinsiTools = ({ tools, setTools }) => { setTools((tools) => [...defaultTools, ...newTools]) } - }, [bsConfig, building]) + }, [bsConfig]) const active = useMemo(() => tools.some(tool => tool.checked), [tools]) @@ -136,7 +135,6 @@ const LinsiTools = ({ tools, setTools }) => { setTools(tools.map(t => t.id === tool.id ? { ...t, checked: val } : t)) } diff --git a/src/frontend/client/src/components/Chat/Input/Files/Table/KnowledgeColumns.tsx b/src/frontend/client/src/components/Chat/Input/Files/Table/KnowledgeColumns.tsx index 04060c2d2..bfe90bdb3 100644 --- a/src/frontend/client/src/components/Chat/Input/Files/Table/KnowledgeColumns.tsx +++ b/src/frontend/client/src/components/Chat/Input/Files/Table/KnowledgeColumns.tsx @@ -49,7 +49,7 @@ export const getKnowledgeColumns = ( return '状态'; }, cell: ({ row }) => { - return ['', '处理中', '成功', '失败'][row.original.status] + return ['', '处理中', '成功', '失败', '模型向量切换中'][row.original.status] }, }, { diff --git a/src/frontend/client/src/components/Chat/Messages/Content/MessageContent.tsx b/src/frontend/client/src/components/Chat/Messages/Content/MessageContent.tsx index 7f473adab..6e4519713 100644 --- a/src/frontend/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/src/frontend/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -60,7 +60,7 @@ export const ErrorMessage = ({ role="alert" aria-live="assertive" className={cn( - 'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200', + 'rounded-xl mt-2 border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200', className, )} > diff --git a/src/frontend/client/src/components/Messages/Content/Error.tsx b/src/frontend/client/src/components/Messages/Content/Error.tsx index c1bea746e..96772c19c 100644 --- a/src/frontend/client/src/components/Messages/Content/Error.tsx +++ b/src/frontend/client/src/components/Messages/Content/Error.tsx @@ -103,7 +103,7 @@ const Error = ({ text }: { text: string }) => { const localize = useLocalize(); const jsonString = extractJson(text); const errorMessage = text.length > 512 && !jsonString ? text.slice(0, 512) + '...' : text; - const defaultResponse = `Something went wrong. Here's the specific error message we encountered: ${errorMessage}`; + const defaultResponse = `${errorMessage}`; if (!isJson(jsonString)) { return defaultResponse; diff --git a/src/frontend/client/src/pages/appChat/ChatMessages.tsx b/src/frontend/client/src/pages/appChat/ChatMessages.tsx index 155fef053..ece7748ce 100644 --- a/src/frontend/client/src/pages/appChat/ChatMessages.tsx +++ b/src/frontend/client/src/pages/appChat/ChatMessages.tsx @@ -30,7 +30,7 @@ export default function ChatMessages({ useName, title, logo }) { const remark = chatState?.flow?.guide_word - return
+ return
{/* 助手开场白 */} {remark && { - filePath && downloadFile(filePath, fileName) + if (filePath) { + const path = filePath.replace(/https?:\/\/[^\/]+/, __APP_ENV__.BASE_URL) + downloadFile(path, fileName) + } } return
{ }, onS
{!(data.reasoning_log && !message && !data.files.length) && <> -
+
{data.sender ?

{data.sender}

:

} -

+
{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}
@@ -92,7 +92,7 @@ export default function MessageBs({ logo, title, data, onUnlike = () => { }, onS } {/* 附加信息 */} { - data.end &&
+ data.end &&
!!data.message.hisValue || false, [data.message.hisValue]) const handleSend = () => { const val = textRef.current.value if (!val.trim()) return - setInputSended(true) emitAreaTextEvent({ action: EVENT_TYPE.MESSAGE_INPUT, data: { nodeId: data.message.node_id, diff --git a/src/frontend/client/src/pages/appChat/components/MessageUser.tsx b/src/frontend/client/src/pages/appChat/components/MessageUser.tsx index 65ed94597..8a0eb7fb0 100644 --- a/src/frontend/client/src/pages/appChat/components/MessageUser.tsx +++ b/src/frontend/client/src/pages/appChat/components/MessageUser.tsx @@ -29,7 +29,7 @@ export default function MessageUser({ useName, data, showButton }) { return
-
+
{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}
diff --git a/src/frontend/client/src/pages/appChat/useChatHelpers.ts b/src/frontend/client/src/pages/appChat/useChatHelpers.ts index 65907fb50..92e1a2541 100644 --- a/src/frontend/client/src/pages/appChat/useChatHelpers.ts +++ b/src/frontend/client/src/pages/appChat/useChatHelpers.ts @@ -8,9 +8,9 @@ import { Chat } from "~/@types/chat" import { baseMsgItem } from "~/api/apps" import { formatDate, generateUUID } from "~/utils" import { FLOW_TYPES } from "." +import { SkillMethod } from "./appUtils/skillMethod" import { bishengConfState, chatIdState, chatsState, currentChatState, currentRunningState, runningState } from "./store/atoms" import { emitAreaTextEvent, EVENT_TYPE } from "./useAreaText" -import { SkillMethod } from "./appUtils/skillMethod" export default function useChatHelpers() { const chatState = useRecoilValue(currentChatState) @@ -171,7 +171,7 @@ export default function useChatHelpers() { // 兼容后端问题 const _files = Array.isArray(files) ? files : [] - const messageId = message_id || (category === "guide_word" ? generateUUID(4) : "") + const messageId = message_id || (category === "guide_word" ? 'u-' + generateUUID(6) : "") const filteredMessages = deduplicateMessages(messages, message_id) return [ @@ -184,7 +184,7 @@ export default function useChatHelpers() { files: _files.map(el => ({ // 兼容 file_name: el.file_name || el.name, - file_url: el.file_url || el.url + file_url: el.file_url || el.url || el.path })), is_bot, message: msg, @@ -321,13 +321,40 @@ export default function useChatHelpers() { { ...baseMsgItem, category: "question", - id: generateUUID(8), + id: 'u-' + generateUUID(8), message: msg, create_time: formatDate(new Date(), "yyyy-MM-ddTHH:mm:ss"), }, ]), ) + + // 滚动到底部 + const dom = document.getElementById('messageScrollPanne') + setTimeout(() => { + if (dom) { + dom.scrollTop = dom.scrollHeight + } + }, 0); }, + closeOutputMsg: (input) => { + setChats((prev) => + updateChatMessages(prev, chatId, (messages) => { + const updatedMessages = messages.map(msg => { + if (["output_with_input_msg", "output_with_choose_msg"].includes(msg.category)) { + return { + ...msg, + message: { + ...msg.message, + hisValue: input + } + } + } + return msg + }) + return updatedMessages + }), + ) + } } return { diff --git a/src/frontend/client/src/pages/appChat/useMessages.ts b/src/frontend/client/src/pages/appChat/useMessages.ts index d9a195b9f..f09abfc8c 100644 --- a/src/frontend/client/src/pages/appChat/useMessages.ts +++ b/src/frontend/client/src/pages/appChat/useMessages.ts @@ -47,6 +47,9 @@ const useLoadMessage = (chatId: string, chatState: any, messageScrollRef: React. const loadMore = async (chatId: string) => { // 运行中or没有更多 || 最后一条消息id不存在,忽略 if (running || historyEnd || !messages?.[0]?.id || !flow) return + const messageId = messages[0].id + // u-开头的消息表示新建会话,新建会话无需加载历史消息 + if (typeof messageId === 'string' && messageId.startsWith('u-')) return const msgs = await getChatHistoryApi(flow.id, chatId, flow.flow_type, messages[0].id || 0) setChats((prev) => { diff --git a/src/frontend/client/src/pages/appChat/useWebsocket.ts b/src/frontend/client/src/pages/appChat/useWebsocket.ts index d658a0d2f..1b3e48161 100644 --- a/src/frontend/client/src/pages/appChat/useWebsocket.ts +++ b/src/frontend/client/src/pages/appChat/useWebsocket.ts @@ -325,6 +325,8 @@ export const useWebSocket = (helpers) => { } }, }) + // 闭合所有输出节点 + helpers.message.closeOutputMsg(submitData.data.data.output_result) break; case ActionType.STOP: sendWsMsg({ action: 'stop' }) diff --git a/src/frontend/client/src/pages/apps/components/AgentCard.tsx b/src/frontend/client/src/pages/apps/components/AgentCard.tsx index e1d2d0f9e..61a3eacf8 100644 --- a/src/frontend/client/src/pages/apps/components/AgentCard.tsx +++ b/src/frontend/client/src/pages/apps/components/AgentCard.tsx @@ -63,7 +63,7 @@ export function AgentCard({ {/* 2. 描述区域:占满剩余空间,最多显示 2 行(适配 150px 高度) */}

{/* 文字缩小为 xs,最多 2 行 */} - {agent.description || "无描述信息"} {/* 空描述兜底,避免高度塌陷 */} + {agent.description} {/* 空描述兜底,避免高度塌陷 */}

diff --git a/src/frontend/client/src/pages/apps/components/AgentGrid.tsx b/src/frontend/client/src/pages/apps/components/AgentGrid.tsx index a12a8b4c0..62452514a 100644 --- a/src/frontend/client/src/pages/apps/components/AgentGrid.tsx +++ b/src/frontend/client/src/pages/apps/components/AgentGrid.tsx @@ -2,7 +2,7 @@ import { ChevronDown, Loader2 } from "lucide-react" import type React from "react" -import { useEffect, useState } from "react" +import { useEffect, useState, useCallback } from "react" import { getChatOnlineApi, getFrequently, getHomeLabelApi, getUncategorized } from "~/api/apps" import { Button } from "~/components" import { AgentCard } from "./AgentCard" @@ -26,6 +26,7 @@ interface AgentGridProps { sectionRefs: React.MutableRefObject> refreshTrigger: number onCardClick: (agent: Agent) => void + onSectionMounted: (id: string, element: HTMLElement | null) => void // 新增回调函数 } // 分类标签类型 @@ -49,7 +50,8 @@ export function AgentGrid({ onRemoveFromFavorites, sectionRefs, refreshTrigger, - onCardClick + onCardClick, + onSectionMounted // 新增回调函数 }: AgentGridProps) { const pageSize = 8 // 固定单页容量 const [categories, setCategories] = useState([]) @@ -435,11 +437,11 @@ const handleAddToFavorites = async (type: number, id: string) => { return (
{ + ref={(el) => { sectionRefs.current[id] = el - }} - > + }} > {/* 分区标题 */}

{name}

diff --git a/src/frontend/client/src/pages/apps/components/SearchOverlay.tsx b/src/frontend/client/src/pages/apps/components/SearchOverlay.tsx index d10084f0a..156d0e618 100644 --- a/src/frontend/client/src/pages/apps/components/SearchOverlay.tsx +++ b/src/frontend/client/src/pages/apps/components/SearchOverlay.tsx @@ -128,7 +128,7 @@ export function SearchOverlay({ agent={agent} isFavorite={favorites ? favorites.includes(agent.id.toString()) : false} showRemove={false} - onAddToFavorites={() => onAddToFavorites(agent.id.toString())} + onAddToFavorites={() => onAddToFavorites(agent.flow_type,agent.id.toString())} onRemoveFromFavorites={() => onRemoveFromFavorites(agent.id.toString())} /> ))} diff --git a/src/frontend/client/src/pages/apps/index.tsx b/src/frontend/client/src/pages/apps/index.tsx index 67954970f..acdceb130 100644 --- a/src/frontend/client/src/pages/apps/index.tsx +++ b/src/frontend/client/src/pages/apps/index.tsx @@ -110,24 +110,56 @@ export default function AgentCenter() { } }; - const handleSearchChange = async (query: string) => { - if (query.trim()) { - setIsSearching(true) - setSearchLoading(true) - try { - const result = await getChatOnlineApi(1, query, -1, true) - setSearchResults(result.data || []) - } catch (error) { - console.error("搜索失败:", error) - setSearchResults([]) - } finally { - setSearchLoading(false) +// 修改handleSearchChange函数,实现多页数据加载 +const handleSearchChange = async (query: string) => { + if (query.trim()) { + setIsSearching(true); + setSearchLoading(true); + let allResults: any[] = []; // 存储所有页的结果 + let currentPage = 1; + const pageSize = 8; // 每页条数(和接口保持一致) + + try { + // 循环加载所有页数据 + while (true) { + // 调用接口,禁用默认限制(或按实际需要调整) + const result = await getChatOnlineApi( + currentPage, + query, + -1, + true // 禁用默认限制,或根据接口逻辑调整 + ); + + const pageData = result.data || []; + allResults = [...allResults, ...pageData]; + + // 终止条件:当前页数据不足一页,说明已加载完所有数据 + if (pageData.length < pageSize) { + break; + } + + currentPage++; // 加载下一页 } - } else { - setIsSearching(false) - setSearchResults([]) + + // 处理可能的id字段映射(确保id存在) + const formattedResults = allResults.map(item => ({ + ...item, + id: item.id || item.agentId || item.flowId // 兼容不同字段名 + })); + + setSearchResults(formattedResults); + } catch (error) { + console.error("搜索失败:", error); + setSearchResults([]); + } finally { + setSearchLoading(false); } + } else { + setIsSearching(false); + setSearchResults([]); } +}; + const handleSearch = useDebounce(handleSearchChange, 360, false) const handleSearchClear = () => { @@ -253,7 +285,7 @@ export default function AgentCenter() { {/* Scrollable Content */}
- {!isSearching ? ( + - ) : ( + {isSearching &&( { - const str = id + '' + const str = (id + '').substring(0, 4) let hex = ''; for (let i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16); diff --git a/src/frontend/platform/src/components/bs-comp/chatComponent/MessageBs.tsx b/src/frontend/platform/src/components/bs-comp/chatComponent/MessageBs.tsx index e6e60be06..5d4682cf5 100644 --- a/src/frontend/platform/src/components/bs-comp/chatComponent/MessageBs.tsx +++ b/src/frontend/platform/src/components/bs-comp/chatComponent/MessageBs.tsx @@ -54,7 +54,7 @@ export const ReasoningLog = ({ loading, msg = '' }) => {
} -export default function MessageBs({ debug, mark = false, logo, data, onUnlike = () => { }, onSource, onMarkClick,chat }: { logo: string, data: ChatMessageType, onUnlike?: any, onSource?: any }) { +export default function MessageBs({ debug, mark = false, logo, data, onUnlike = () => { }, onSource, onMarkClick, chat }: { logo: string, data: ChatMessageType, onUnlike?: any, onSource?: any }) { const avatarColor = colorList[ (data.sender?.split('').reduce((num, s) => num + s.charCodeAt(), 0) || 0) % colorList.length ] @@ -84,6 +84,7 @@ export default function MessageBs({ debug, mark = false, logo, data, onUnlike =
+ {logo} {data.message.toString() ?
{} diff --git a/src/frontend/platform/src/components/bs-comp/chatComponent/messageStore.ts b/src/frontend/platform/src/components/bs-comp/chatComponent/messageStore.ts index 8c834ea3c..c47875235 100644 --- a/src/frontend/platform/src/components/bs-comp/chatComponent/messageStore.ts +++ b/src/frontend/platform/src/components/bs-comp/chatComponent/messageStore.ts @@ -62,9 +62,21 @@ const handleHistoryMsg = (data: any[]): ChatMessageType[] => { // 未考虑的情况暂不处理 console.error('消息 to JSON error :>> ', e); } + + // hack + let chatKey = undefined + if (typeof message !== 'string') { + // 优先 input + if ('input' in message) { + chatKey = 'input' + } else { + Object.keys(message)[0] + } + } + return { ...other, - chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0], + chatKey, end: true, files: files ? JSON.parse(files) : [], isSend: !is_bot, diff --git a/src/frontend/platform/src/controllers/API/assistant.ts b/src/frontend/platform/src/controllers/API/assistant.ts index c989b9e99..a7ff46f21 100644 --- a/src/frontend/platform/src/controllers/API/assistant.ts +++ b/src/frontend/platform/src/controllers/API/assistant.ts @@ -52,7 +52,7 @@ export const saveAssistanttApi = async ( ): Promise => { if (data.logo) { // logo保存相对路径 - data.logo = data.logo.replace('/bisheng', '') + data.logo = data.logo.replace(/^\/\w+/, '') } return await axios.put(`/api/v1/assistant`, data) }; diff --git a/src/frontend/platform/src/controllers/API/flow.ts b/src/frontend/platform/src/controllers/API/flow.ts index 7f64b158b..7aadd666a 100644 --- a/src/frontend/platform/src/controllers/API/flow.ts +++ b/src/frontend/platform/src/controllers/API/flow.ts @@ -118,6 +118,13 @@ export async function getFlowApi(flowId: string, version: string = 'v1'): Promis return await axios.get(`/api/${version}/flows/${flowId}`) } +/** + * 删除的技能 工作流详情 + */ +export async function getDeleteFlowApi(chatId: string): Promise { + return await axios.get(`/api/v1/chat/info?chat_id=${chatId}`) +} + /** * Saves a new flow to the database. * @@ -194,7 +201,7 @@ export const createCustomFlowApi = async (params: { }, userName: string) => { if (params.logo) { // logo保存相对路径 - params.logo = params.logo.replace('/bisheng', '') + params.logo = params.logo.replace(/^\/\w+/, '') } const response: FlowType = await axios.post("/api/v1/flows/", { ...params, @@ -221,7 +228,7 @@ export async function updateFlowApi( ): Promise { if (updatedFlow.logo) { // logo保存相对路径 - updatedFlow.logo = updatedFlow.logo.replace('/bisheng', '') + updatedFlow.logo = updatedFlow.logo.replace(/^\/\w+/, '') } return await axios.patch(`/api/v1/flows/${updatedFlow.id}`, { logo: updatedFlow.logo || '', diff --git a/src/frontend/platform/src/controllers/API/workflow.ts b/src/frontend/platform/src/controllers/API/workflow.ts index 047d0d855..b4942f043 100644 --- a/src/frontend/platform/src/controllers/API/workflow.ts +++ b/src/frontend/platform/src/controllers/API/workflow.ts @@ -41,7 +41,7 @@ export const createWorkflowApi = async (name, desc, url, flow): Promise => export const saveWorkflow = async (versionId: number, data: WorkFlow): Promise => { if (data.logo) { // logo保存相对路径 - data.logo = data.logo.replace('/bisheng', '') + data.logo = data.logo.replace(/^\/\w+/, '') } return await axios.put(`/api/v1/workflow/versions/${versionId}`, data); } @@ -111,7 +111,7 @@ export async function getWorkFlowVersions(flow_id): Promise<{ data: any[], total */ export const onlineWorkflow = async (flow, status = ''): Promise => { const { name, description, logo } = flow - const data = { name, description, logo: logo && logo.replace('/bisheng', '') } + const data = { name, description, logo: logo && logo.replace(/^\/\w+/, '') } if (status) { data['status'] = status } diff --git a/src/frontend/platform/src/pages/BuildPage/CreateApp.tsx b/src/frontend/platform/src/pages/BuildPage/CreateApp.tsx index fc51e6a2e..a4203f8ff 100644 --- a/src/frontend/platform/src/pages/BuildPage/CreateApp.tsx +++ b/src/frontend/platform/src/pages/BuildPage/CreateApp.tsx @@ -16,7 +16,7 @@ import { copyReportTemplate, createWorkflowApi } from "@/controllers/API/workflo import { captureAndAlertRequestErrorHoc } from "@/controllers/request"; import { uploadFileWithProgress } from "@/modals/UploadModal/upload"; import { AppType, AppTypeToNum } from "@/types/app"; -import { forwardRef, useContext, useImperativeHandle, useRef, useState } from "react"; +import { forwardRef, useContext, useImperativeHandle, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -230,10 +230,14 @@ ${t('build.exampleTwo', { ns: 'bs' })} // 上传头像逻辑 const uploadAvator = (file: File) => { uploadFileWithProgress(file, (progress) => { }, 'icon').then(res => { - setFormData(prev => ({ ...prev, url: '/bisheng/' + res.relative_path })); + setFormData(prev => ({ ...prev, url: res.file_path })); }); }; + const previewAvatar = useMemo(() => + formData.url ? __APP_ENV__.BASE_URL + formData.url : '', + [formData.url]) + return ( @@ -245,8 +249,8 @@ ${t('build.exampleTwo', { ns: 'bs' })} - - + +
diff --git a/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/EditAssistantDialog.tsx b/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/EditAssistantDialog.tsx index 2eee5f48a..1cef5fff8 100644 --- a/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/EditAssistantDialog.tsx +++ b/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/EditAssistantDialog.tsx @@ -5,7 +5,7 @@ import { Input, Textarea } from "@/components/bs-ui/input"; import Avator from "@/components/bs-ui/input/avator"; import { useToast } from "@/components/bs-ui/toast/use-toast"; import { uploadFileWithProgress } from "@/modals/UploadModal/upload"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; export default function EditAssistantDialog({ logo, name, desc, onSave, loca }) { @@ -84,6 +84,10 @@ export default function EditAssistantDialog({ logo, name, desc, onSave, loca }) }) } + const previewAvatar = useMemo(() => + formData.logo ? __APP_ENV__.BASE_URL + formData.logo : '', + [formData.logo]) + return {t('build.editAssistant')} @@ -92,11 +96,11 @@ export default function EditAssistantDialog({ logo, name, desc, onSave, loca })
- + {errors.name &&

{errors.name}

} diff --git a/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/Header.tsx b/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/Header.tsx index eb5d21d8d..077d645d0 100644 --- a/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/Header.tsx +++ b/src/frontend/platform/src/pages/BuildPage/assistant/editAssistant/Header.tsx @@ -37,7 +37,7 @@ export default function Header({ loca, onSave, onLine, onTabChange }) { return
- + {assistantState.name} {/* edit dialog */} diff --git a/src/frontend/platform/src/pages/BuildPage/flow/FlowChat/messageStore.ts b/src/frontend/platform/src/pages/BuildPage/flow/FlowChat/messageStore.ts index 53f476eef..217011817 100644 --- a/src/frontend/platform/src/pages/BuildPage/flow/FlowChat/messageStore.ts +++ b/src/frontend/platform/src/pages/BuildPage/flow/FlowChat/messageStore.ts @@ -68,10 +68,21 @@ const handleHistoryMsg = (data: any[]): ChatMessageType[] => { // 未考虑的情况暂不处理 console.error('消息 to JSON error :>> ', e); } + // hack + let chatKey = undefined + if (typeof message !== 'string') { + // 优先 input + if ('input' in message) { + chatKey = 'input' + } else { + Object.keys(message)[0] + } + } + return { ...other, category, - chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0], + chatKey, end: true, files: files ? JSON.parse(files) : [], isSend: !is_bot, diff --git a/src/frontend/platform/src/pages/BuildPage/skills/l2Edit.tsx b/src/frontend/platform/src/pages/BuildPage/skills/l2Edit.tsx index a818a293b..d231720b8 100644 --- a/src/frontend/platform/src/pages/BuildPage/skills/l2Edit.tsx +++ b/src/frontend/platform/src/pages/BuildPage/skills/l2Edit.tsx @@ -144,10 +144,14 @@ export default function l2Edit() { const [logo, setLogo] = useState('') const uploadAvator = (file) => { uploadFileWithProgress(file, (progress) => { }, 'icon').then(res => { - setLogo('/bisheng/' + res.relative_path); + setLogo(res.file_path); }) } + const previewAvatar = useMemo(() => + logo ? __APP_ENV__.BASE_URL + logo : '', + [logo]) + return
@@ -174,8 +178,8 @@ export default function l2Edit() {
- - + +
diff --git a/src/frontend/platform/src/pages/ChatAppPage/components/ChatPanne.tsx b/src/frontend/platform/src/pages/ChatAppPage/components/ChatPanne.tsx index 5ad5d336e..802849c5c 100644 --- a/src/frontend/platform/src/pages/ChatAppPage/components/ChatPanne.tsx +++ b/src/frontend/platform/src/pages/ChatAppPage/components/ChatPanne.tsx @@ -281,8 +281,8 @@ export default function ChatPanne({ customWsHost = '', chatList, chat, appendHis { workflow &&
- - {chat.flow_name} + + {workflow.name}
diff --git a/src/frontend/platform/src/pages/DataSetPage/CreateDataSet.tsx b/src/frontend/platform/src/pages/DataSetPage/CreateDataSet.tsx index 6b3fdbc75..30cd13fbf 100644 --- a/src/frontend/platform/src/pages/DataSetPage/CreateDataSet.tsx +++ b/src/frontend/platform/src/pages/DataSetPage/CreateDataSet.tsx @@ -169,7 +169,7 @@ const CreateDataSet = forwardRef(({ onChange }, ref) => {
{ - const errorlist = []; - if (!selectedType) errorlist.push(t("evaluation.enterExecType")); - // if (!selectedKeyId) errorlist.push(t("evaluation.workFlow")); + const errorlist = []; + if (!selectedType) errorlist.push(t("evaluation.enterExecType")); + + if (selectedType && !selectedKeyId) { + if (selectedType === "workflow") errorlist.push(t("请选择工作流")); + if (selectedType === "flow") errorlist.push(t("请选择技能")); + if (selectedType === "assistant") errorlist.push(t("请选择助手")); + } - // 修复版本验证 - 取消注释并添加正确的验证 - if ((selectedType === "workflow" || selectedType === "flow" || !selectedKeyId) && !selectedVersion) { - errorlist.push(t("evaluation.workFlow")); - } + if (selectedKeyId && (selectedType === "workflow" || selectedType === "flow") && !selectedVersion) { + errorlist.push(t("请选择版本")); + } + if (!fileRef.current && selectedKeyId&&selectedVersion) errorlist.push(t("evaluation.enterFile")); + if (!prompt) errorlist.push(t("evaluation.enterPrompt")); - // 修复文件验证 - 所有类型都需要测试集数据 - // if (!fileRef.current) errorlist.push(t("evaluation.enterFile")); - - if (!prompt) errorlist.push(t("evaluation.enterPrompt")); - - if (errorlist.length) return handleError(errorlist); + if (errorlist.length) return handleError(errorlist); setLoading(true); try { await captureAndAlertRequestErrorHoc( diff --git a/src/frontend/platform/src/pages/KnowledgePage/AdjustFilesUpload.tsx b/src/frontend/platform/src/pages/KnowledgePage/AdjustFilesUpload.tsx index d7bed3f32..6b63ba93f 100644 --- a/src/frontend/platform/src/pages/KnowledgePage/AdjustFilesUpload.tsx +++ b/src/frontend/platform/src/pages/KnowledgePage/AdjustFilesUpload.tsx @@ -5,7 +5,7 @@ import { useToast } from "@/components/bs-ui/toast/use-toast"; import { rebUploadFile, retryKnowledgeFileApi, subUploadLibFile } from "@/controllers/API"; import { captureAndAlertRequestErrorHoc } from "@/controllers/request"; import { ChevronLeft } from "lucide-react"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import FileUploadStep2 from "./components/FileUploadStep2"; @@ -29,6 +29,12 @@ export default function AdjustFilesUpload() { // 从路由状态获取调整模式的初始数据(必须传文件数据) const initFileData = location.state?.fileData; + useEffect(() => { + // 如果没有初始化数据,说明是直接访问,跳转到/filelib + if (!initFileData) { + navigate('/filelib', { replace: true }); + } + }, [initFileData, navigate]); if (!initFileData) { navigate(-1); // 无数据时回退 return null; @@ -112,17 +118,18 @@ export default function AdjustFilesUpload() { const fileName = initFileData.name || initFileData.file_name || ''; const fileSuffix = fileName.split('.').pop()?.toLowerCase() || 'txt'; const fileType = ['xlsx', 'xls', 'csv'].includes(fileSuffix) ? 'table' : 'file'; + const [resultFiles, setResultFiles] = useState([ { id: initFileData.id, fileName: initFileData.name || initFileData.file_name, // 兼容字段名 file_path: initFileData.filePath || initFileData.object_name, // 兼容文件路径 suffix: initFileData.suffix || initFileData.file_name?.split(".").pop() || "", // 解析后缀 + previewUrl:initFileData.previewUrl, fileType: fileType, split_rule: getParsedSplitRule(initFileData.split_rule) // 传入转换后的配置对象 } ]); - console.log(initFileData,78); const [segmentRules, setSegmentRules] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/src/frontend/platform/src/pages/KnowledgePage/KnowledgeFile.tsx b/src/frontend/platform/src/pages/KnowledgePage/KnowledgeFile.tsx index 1952ef25e..fbad75444 100644 --- a/src/frontend/platform/src/pages/KnowledgePage/KnowledgeFile.tsx +++ b/src/frontend/platform/src/pages/KnowledgePage/KnowledgeFile.tsx @@ -23,7 +23,7 @@ import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogT import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/bs-ui/select"; import { useToast } from "@/components/bs-ui/toast/use-toast"; import { getKnowledgeModelConfig, getLLmServerDetail, getModelListApi } from "@/controllers/API/finetune"; -import { CircleAlert, Copy, Ellipsis, LoaderCircle, Settings, Trash2 } from "lucide-react"; +import { BookCopy, CircleAlert, Copy, Ellipsis, LoaderCircle, Settings, Trash2 } from "lucide-react"; import AutoPagination from "../../components/bs-ui/pagination/autoPagination"; import { useTable } from "../../util/hook"; import { ModelSelect } from "../ModelPage/manage/tabs/WorkbenchModel"; @@ -112,7 +112,7 @@ function CreateModal({ datalist, open, onOpenChange, onLoadEnd, mode = 'create', fetchModelData(); }, [open, mode, currentLib]); - + useEffect(() => { // 当弹窗关闭时,清空所有内部状态 if (!open) { @@ -122,112 +122,112 @@ function CreateModal({ datalist, open, onOpenChange, onLoadEnd, mode = 'create', setError({ name: false, desc: false }); } }, [open]); - + const { toast } = useToast() const [error, setError] = useState({ name: false, desc: false }) - const handleCreate = async (e, isImport = false) => { - const name = nameRef.current.value || ''; // 名称(默认空字符串,避免null) - let desc = descRef.current.value || ''; // 描述(默认空字符串) + const handleCreate = async (e, isImport = false) => { + const name = nameRef.current.value || ''; // 名称(默认空字符串,避免null) + let desc = descRef.current.value || ''; // 描述(默认空字符串) - // 1. 定义默认描述的"固定文本部分"(不含名称) - const defaultDescPrefix = "当回答与"; - const defaultDescSuffix = "相关的问题时,参考此知识库"; - // 固定文本总长度 = 前缀长度 + 后缀长度 - const fixedTextLength = defaultDescPrefix.length + defaultDescSuffix.length; - // 名称可占用的最大长度 = 200 - 固定文本长度(确保名称+固定文本≤200) - const maxNameLengthForDefaultDesc = 200 - fixedTextLength; + // 1. 定义默认描述的"固定文本部分"(不含名称) + const defaultDescPrefix = "当回答与"; + const defaultDescSuffix = "相关的问题时,参考此知识库"; + // 固定文本总长度 = 前缀长度 + 后缀长度 + const fixedTextLength = defaultDescPrefix.length + defaultDescSuffix.length; + // 名称可占用的最大长度 = 200 - 固定文本长度(确保名称+固定文本≤200) + const maxNameLengthForDefaultDesc = 200 - fixedTextLength; - // 2. 未输入描述时,生成默认描述(严格控制总长度≤200) - if (!desc) { - // 情况1:名称长度 ≤ 可占用最大长度 → 直接拼接生成默认描述 - if (name.length <= maxNameLengthForDefaultDesc) { - desc = `${defaultDescPrefix}${name}${defaultDescSuffix}`; - } - // 情况2:名称长度 > 可占用最大长度 → 截断名称后再拼接 - else { - // 截断名称(保留前 maxNameLengthForDefaultDesc 个字,避免总长度超200) - const truncatedName = name.slice(0, maxNameLengthForDefaultDesc); - // 生成截断截断后的默认描述 - desc = `${defaultDescPrefix}${truncatedName}${defaultDescSuffix}`; - // 提示用户:名称过长已被截断(提升体验,避免用户困惑) - toast({ - variant: "info", - description: `知识库名称过长,已自动截断为${maxNameLengthForDefaultDesc}字,确保默认描述不超过200字限制` - }); + // 2. 未输入描述时,生成默认描述(严格控制总长度≤200) + if (!desc) { + // 情况1:名称长度 ≤ 可占用最大长度 → 直接拼接生成默认描述 + if (name.length <= maxNameLengthForDefaultDesc) { + desc = `${defaultDescPrefix}${name}${defaultDescSuffix}`; + } + // 情况2:名称长度 > 可占用最大长度 → 截断名称后再拼接 + else { + // 截断名称(保留前 maxNameLengthForDefaultDesc 个字,避免总长度超200) + const truncatedName = name.slice(0, maxNameLengthForDefaultDesc); + // 生成截断截断后的默认描述 + desc = `${defaultDescPrefix}${truncatedName}${defaultDescSuffix}`; + // 提示用户:名称过长已被截断(提升体验,避免用户困惑) + toast({ + variant: "info", + description: `知识库名称过长,已自动截断为${maxNameLengthForDefaultDesc}字,确保默认描述不超过200字限制` + }); + } } - } - // 3. 原有校验逻辑(仅针对用户手动输入的描述,默认描述已确保≤200) - if (!name) { - handleError(t('lib.enterLibraryName')); - return; - } - if (name.length > 30) { - handleError('知识库名称不能超过30字'); - return; - } - if (!modal) { - handleError(t('lib.selectModel')); - return; - } - - // 修复:名称重复校验逻辑 - // 编辑模式且名称未变更时,不进行重复校验 - const isEditMode = mode === 'edit' && currentLib; - const nameUnchanged = isEditMode && name === currentLib.name; - - if (!nameUnchanged && datalist.find(data => data.name === name && (!currentLib || data.id !== currentLib.id))) { - handleError(t('lib.nameExists')); - return; - } - - // 仅校验用户手动输入的描述(默认描述已控制长度,可跳过) - if (descRef.current.value && desc.length > 200) { - handleError(t('lib.descriptionLimit')); - return; - } - - setIsSubmitting(true) - if (mode === 'create') { - await captureAndAlertRequestErrorHoc(createFileLib({ - name, - description: desc, - model: modal[1].value, - type: 0 - }).then(res => { - window.libname = [name, desc] - navigate(isImport - ? `/filelib/upload/${res.id}` - : `/filelib/${res.id}` - ); - onOpenChange(false); // 修复:用onOpenChange关闭弹窗 - })).finally(() => { - setIsSubmitting(false) - }) - } else { - const data = { - "model_id": modal[1].value, - "model_type": "embedding", - "knowledge_id": currentLib.id, - "knowledge_name": name, - "description": desc + // 3. 原有校验逻辑(仅针对用户手动输入的描述,默认描述已确保≤200) + if (!name) { + handleError(t('lib.enterLibraryName')); + return; } - await captureAndAlertRequestErrorHoc(updateKnowledge(data).then(res => { - toast({ - variant: "success", - description: '更新成功' + if (name.length > 200) { + handleError('知识库名称不能超过200字'); + return; + } + if (!modal) { + handleError(t('lib.selectModel')); + return; + } + + // 修复:名称重复校验逻辑 + // 编辑模式且名称未变更时,不进行重复校验 + const isEditMode = mode === 'edit' && currentLib; + const nameUnchanged = isEditMode && name === currentLib.name; + + if (!nameUnchanged && datalist.find(data => data.name === name && (!currentLib || data.id !== currentLib.id))) { + handleError(t('lib.nameExists')); + return; + } + + // 仅校验用户手动输入的描述(默认描述已控制长度,可跳过) + if (descRef.current.value && desc.length > 200) { + handleError(t('lib.descriptionLimit')); + return; + } + + setIsSubmitting(true) + if (mode === 'create') { + await captureAndAlertRequestErrorHoc(createFileLib({ + name, + description: desc, + model: modal[1].value, + type: 0 + }).then(res => { + window.libname = [name, desc] + navigate(isImport + ? `/filelib/upload/${res.id}` + : `/filelib/${res.id}` + ); + onOpenChange(false); // 修复:用onOpenChange关闭弹窗 + })).finally(() => { + setIsSubmitting(false) }) - onOpenChange(false); // 修复:用onOpenChange关闭弹窗(替代原setOpen) - onLoadEnd() - }).catch(error => { - toast({ variant: "error", description: '更新失败,请重试' }); - onOpenChange(false); // 错误时也关闭弹窗,避免状态卡住 - })).finally(() => { - setIsSubmitting(false) - }) + } else { + const data = { + "model_id": modal[1].value, + "model_type": "embedding", + "knowledge_id": currentLib.id, + "knowledge_name": name, + "description": desc + } + await captureAndAlertRequestErrorHoc(updateKnowledge(data).then(res => { + toast({ + variant: "success", + description: '更新成功' + }) + onOpenChange(false); // 修复:用onOpenChange关闭弹窗(替代原setOpen) + onLoadEnd() + }).catch(error => { + toast({ variant: "error", description: '更新失败,请重试' }); + onOpenChange(false); // 错误时也关闭弹窗,避免状态卡住 + })).finally(() => { + setIsSubmitting(false) + }) + } } -} const handleError = (message) => { toast({ @@ -411,21 +411,21 @@ export default function KnowledgeFile() { }) } -const handleOpenSettings = (lib) => { - console.log("=== handleOpenSettings 开始执行 ==="); - console.log("当前点击的lib ID:", lib.id); - // 1. 深拷贝:彻底断开与原 lib 的引用关联(解决嵌套属性引用不变问题) - const newCurrentLib = JSON.parse(JSON.stringify(lib)); - // 2. 注入唯一标识:即使数据完全相同,也让 currentSettingLib 引用绝对唯一 - newCurrentLib.__updateKey = Date.now(); // 每次点击生成不同的时间戳 - - setCurrentSettingLib(newCurrentLib); // 此时传递的是完全新的对象引用 - setSettingsOpen(true); - setModalKey(prev => prev + 1); // 保留 modalKey 确保弹窗重新挂载 - console.log("handleOpenSettings called with lib:", newCurrentLib); // 验证打印 -}; + const handleOpenSettings = (lib) => { + console.log("=== handleOpenSettings 开始执行 ==="); + console.log("当前点击的lib ID:", lib.id); + // 1. 深拷贝:彻底断开与原 lib 的引用关联(解决嵌套属性引用不变问题) + const newCurrentLib = JSON.parse(JSON.stringify(lib)); + // 2. 注入唯一标识:即使数据完全相同,也让 currentSettingLib 引用绝对唯一 + newCurrentLib.__updateKey = Date.now(); // 每次点击生成不同的时间戳 - const handleSettingsClose = (isOpen) => { + setCurrentSettingLib(newCurrentLib); // 此时传递的是完全新的对象引用 + setSettingsOpen(true); + setModalKey(prev => prev + 1); // 保留 modalKey 确保弹窗重新挂载 + console.log("handleOpenSettings called with lib:", newCurrentLib); // 验证打印 + }; + + const handleSettingsClose = (isOpen) => { console.log("handleSettingsClose called with isOpen:", isOpen); setSettingsOpen(isOpen); if (!isOpen) { @@ -439,7 +439,7 @@ const handleOpenSettings = (lib) => { const handleCachePage = () => { window.LibPage = { page, type: 'file' } } - + useEffect(() => { const _page = window.LibPage if (_page) { @@ -511,7 +511,7 @@ const handleOpenSettings = (lib) => { key={el.id} className=" h-[70px]" > - { window.libname = [el.name, el.description]; @@ -520,27 +520,27 @@ const handleOpenSettings = (lib) => { }} >
-
- +
+
{el.name}
- {el.description || ''} + {el.description || ''}
- { window.libname = [el.name, el.description]; @@ -551,7 +551,7 @@ const handleOpenSettings = (lib) => { {el.update_time.replace('T', ' ')} - { window.libname = [el.name, el.description]; @@ -565,15 +565,15 @@ const handleOpenSettings = (lib) => {