Merge branch 'feat/2.1.0' of https://github.com/dataelement/bisheng into feat/2.1.0

This commit is contained in:
brainlds@163.com
2025-09-12 10:15:52 +08:00
47 changed files with 527 additions and 614 deletions
+4 -1
View File
@@ -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]
+2 -11
View File
@@ -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):
@@ -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
@@ -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 <think>...</think> 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 <think> 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 <think> 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 <think> 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 </think> 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 </think> 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 </think> 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
+2
View File
@@ -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",
+3 -3
View File
@@ -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}'
@@ -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
pass
@@ -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: <Pencil size="16" />,
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 }) => {
</div>
<Switch className='data-[state=checked]:bg-blue-600'
checked={tool.checked}
disabled={tool.id === 'knowledge' && building}
onCheckedChange={val =>
setTools(tools.map(t => t.id === tool.id ? { ...t, checked: val } : t))
}
@@ -49,7 +49,7 @@ export const getKnowledgeColumns = (
return '状态';
},
cell: ({ row }) => {
return ['', '处理中', '成功', '失败'][row.original.status]
return ['', '处理中', '成功', '失败', '模型向量切换中'][row.original.status]
},
},
{
@@ -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,
)}
>
@@ -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;
@@ -30,7 +30,7 @@ export default function ChatMessages({ useName, title, logo }) {
const remark = chatState?.flow?.guide_word
return <div ref={messageScrollRef} className="h-full overflow-y-auto scrollbar-hide pt-12 pb-96 px-4">
return <div id="messageScrollPanne" ref={messageScrollRef} className="h-full overflow-y-auto scrollbar-hide pt-12 pb-96 px-4">
{/* 助手开场白 */}
{remark && <MessageRemark
logo={logo}
@@ -4,7 +4,10 @@ export default function ChatFile({ fileName, filePath }) {
// download file
const handleDownloadFile = (filePath) => {
filePath && downloadFile(filePath, fileName)
if (filePath) {
const path = filePath.replace(/https?:\/\/[^\/]+/, __APP_ENV__.BASE_URL)
downloadFile(path, fileName)
}
}
return <div
@@ -64,9 +64,9 @@ export default function MessageBs({ logo, title, data, onUnlike = () => { }, onS
<div className="w-fit group max-w-[90%]">
<ReasoningLog loading={!data.end && (data.reasoning_log || reasoningLog)} msg={data.reasoning_log || reasoningLog} />
{!(data.reasoning_log && !message && !data.files.length) && <>
<div className="flex justify-between items-center mb-1">
<div className="flex gap-2 items-center mb-1 ml-2">
{data.sender ? <p className="text-gray-600 text-xs">{data.sender}</p> : <p />}
<div className={`text-right group-hover:opacity-100 opacity-0`}>
<div className={`group-hover:opacity-100 opacity-0`}>
<span className="text-slate-400 text-sm">{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}</span>
</div>
</div>
@@ -92,7 +92,7 @@ export default function MessageBs({ logo, title, data, onUnlike = () => { }, onS
</>}
{/* 附加信息 */}
{
data.end && <div className="flex justify-between mt-2">
data.end && <div className="flex justify-between">
<MessageSource
extra={data.extra || {}}
end={data.end}
@@ -35,11 +35,10 @@ export default function MessageBsChoose({ type = 'choose', logo, data, flow }: {
// input
const textRef = useRef(null)
const [inputSended, setInputSended] = useState(!!data.message.hisValue || false)
const inputSended = useMemo(() => !!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,
@@ -29,7 +29,7 @@ export default function MessageUser({ useName, data, showButton }) {
return <div className="flex w-full">
<div className="w-fit group min-h-8 max-w-[90%]">
<div className="flex justify-end items-center mb-2 gap-2">
<div className="flex justify-start items-center gap-2 ml-4">
<div className={`text-right group-hover:opacity-100 opacity-0`}>
<span className="text-slate-400 text-sm">{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}</span>
</div>
@@ -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 {
@@ -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) => {
@@ -325,6 +325,8 @@ export const useWebSocket = (helpers) => {
}
},
})
// 闭合所有输出节点
helpers.message.closeOutputMsg(submitData.data.data.output_result)
break;
case ActionType.STOP:
sendWsMsg({ action: 'stop' })
@@ -63,7 +63,7 @@ export function AgentCard({
{/* 2. 描述区域:占满剩余空间,最多显示 2 行(适配 150px 高度) */}
<div className="flex-1 overflow-hidden">
<p className="text-sm text-[#64748b] leading-5 break-words line-clamp-2"> {/* 文字缩小为 xs,最多 2 行 */}
{agent.description || "无描述信息"} {/* 空描述兜底,避免高度塌陷 */}
{agent.description} {/* 空描述兜底,避免高度塌陷 */}
</p>
</div>
</div>
@@ -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<Record<string, HTMLElement | null>>
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<Category[]>([])
@@ -435,11 +437,11 @@ const handleAddToFavorites = async (type: number, id: string) => {
return (
<section
key={id}
id={id}
className="relative"
ref={(el) => {
ref={(el) => {
sectionRefs.current[id] = el
}}
>
}} >
{/* 分区标题 */}
<h2 className="text-base font-medium mb-4 text-blue-600">{name}</h2>
@@ -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())}
/>
))}
+49 -17
View File
@@ -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 */}
<div className="relative" style={{ height: "calc(100vh - 200px)" }}>
<div ref={scrollContainerRef} className="container mx-auto px-6 py-6 pb-96 h-full overflow-y-auto scrollbar-hide">
{!isSearching ? (
<AgentGrid
favorites={favorites}
onAddToFavorites={addToFavorites}
@@ -262,7 +294,7 @@ export default function AgentCenter() {
refreshTrigger={refreshTrigger}
onCardClick={handleCardClick}
/>
) : (
{isSearching &&(
<SearchOverlay
query={searchQuery}
results={searchResults}
@@ -362,7 +362,7 @@
"enterUniqueId": "请选择具体应用",
"enterVersion": "请选择版本",
"workFlow":"请选择工作流",
"enterFile": "请选择测试集数据",
"enterFile": "请上传测试集数据",
"fileSizeLimit": "文件大小限制在10M以内",
"enterPrompt": "评测指令不能为空",
"evaluationCollection": "评测集合",
@@ -14,7 +14,7 @@ const gradients = [
export default function AppAvator({ id = 1, flowType = '', url = '', className = '' }) {
const color = useMemo(() => {
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);
@@ -54,7 +54,7 @@ export const ReasoningLog = ({ loading, msg = '' }) => {
</div>
}
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 =
</div>
<div className="min-h-8 px-6 py-4 rounded-2xl bg-[#F5F6F8] dark:bg-[#313336]">
<div className="flex gap-2">
{logo}
{data.message.toString() ?
<div ref={messageRef} className="text-sm max-w-[calc(100%-24px)]">
{<MessageMarkDown message={message} />}
@@ -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,
@@ -52,7 +52,7 @@ export const saveAssistanttApi = async (
): Promise<any> => {
if (data.logo) {
// logo保存相对路径
data.logo = data.logo.replace('/bisheng', '')
data.logo = data.logo.replace(/^\/\w+/, '')
}
return await axios.put(`/api/v1/assistant`, data)
};
@@ -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<FlowType> {
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<FlowType> {
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 || '',
@@ -41,7 +41,7 @@ export const createWorkflowApi = async (name, desc, url, flow): Promise<any> =>
export const saveWorkflow = async (versionId: number, data: WorkFlow): Promise<any> => {
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<any> => {
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
}
@@ -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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[625px] bg-background-login">
@@ -245,8 +249,8 @@ ${t('build.exampleTwo', { ns: 'bs' })}
<label htmlFor="name" className="bisheng-label">
{appType === AppType.ASSISTANT ? t('assistantAvatar') : t('workflowAvatar')}
</label>
<Avator value={formData.url} className="mt-3" onChange={uploadAvator}>
<AppAvator id={formData.name} url={loca?.logo} flowType={AppTypeToNum[appType]} className="size-8"></AppAvator>
<Avator value={previewAvatar} className="mt-3" onChange={uploadAvator}>
<AppAvator id={6} flowType={AppTypeToNum[appType]} className="size-8"></AppAvator>
</Avator>
</div>
<div className="mb-6">
@@ -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 <DialogContent className="sm:max-w-[625px] bg-background-login">
<DialogHeader>
<DialogTitle>{t('build.editAssistant')}</DialogTitle>
@@ -92,11 +96,11 @@ export default function EditAssistantDialog({ logo, name, desc, onSave, loca })
<div className="">
<label htmlFor="name" className="bisheng-label">{t('build.assistantAvatar')}<span className="bisheng-tip">*</span></label>
<Avator
value={formData.logo}
value={previewAvatar}
className="mt-2"
onChange={uploadAvator}
>
<AppAvator id={formData.name} url={formData.logo} flowType={5} className="size-8"></AppAvator>
<AppAvator id={6} flowType={5} className="size-9"></AppAvator>
</Avator>
{errors.name && <p className="bisheng-tip mt-1">{errors.name}</p>}
@@ -37,7 +37,7 @@ export default function Header({ loca, onSave, onLine, onTabChange }) {
return <div className="flex justify-between bg-background-login items-center border-b px-4">
<div className="flex items-center gap-2 py-4">
<Button variant="outline" size="icon" onClick={() => navigate(-1)}><ChevronLeft className="h-4 w-4" /></Button>
<AppAvator id={assistantState.name} url={loca?.logo} flowType={5} className="ml-4"></AppAvator>
<AppAvator id={assistantState.name} url={assistantState.logo} flowType={5} className="ml-4"></AppAvator>
<span className="bisheng-title">{assistantState.name}</span>
{/* edit dialog */}
<Dialog open={editShow} onOpenChange={setEditShow}>
@@ -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,
@@ -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 <div className="relative box-border h-full overflow-auto">
<div className="p-6 pb-48 h-full overflow-y-auto">
<div className="flex justify-between w-full">
@@ -174,8 +178,8 @@ export default function l2Edit() {
<div className="w-full overflow-hidden transition-all px-1">
<div className="mt-4">
<Label htmlFor="name">{t('skills.avatar')}</Label>
<Avator value={logo} className="mt-2" onChange={uploadAvator}>
<AppAvator id={name} url={logo} flowType={1} className="w-9 h-9"></AppAvator>
<Avator value={previewAvatar} className="mt-2" onChange={uploadAvator}>
<AppAvator id={6} flowType={1} className="w-9 h-9"></AppAvator>
</Avator>
</div>
<div className="mt-4">
@@ -281,8 +281,8 @@ export default function ChatPanne({ customWsHost = '', chatList, chat, appendHis
{
workflow && <div className={`w-full chat-box h-full relative ${type === AppNumType.FLOW ? 'block' : 'hidden'}`}>
<div className="absolute flex top-2 gap-2 items-center z-10 bg-[rgba(255,255,255,0.8)] px-6 py-1 dark:bg-[#1B1B1B]">
<AppAvator id={chat.flow_name} url={chat.logo} flowType={10} ></AppAvator>
<span className="text-sm">{chat.flow_name}</span>
<AppAvator id={workflow.name} url={workflow.logo} flowType={10} ></AppAvator>
<span className="text-sm">{workflow.name}</span>
</div>
<ChatPane autoRun={autoRun} chatId={flowChatId} flow={workflow} wsUrl={wsUrl} />
</div>
@@ -169,7 +169,7 @@ const CreateDataSet = forwardRef(({ onChange }, ref) => {
</div>
<SimpleUpload
filekey="file"
uploadUrl={__APP_ENV__.BASE_URL + '/api/v1/knowledge/upload'}
uploadUrl={'/api/v1/knowledge/upload'}
accept={['json']}
className={`${error.fileUrl ? 'border-red-400' : ''}`}
onSuccess={handleFileUploadSuccess}
@@ -114,21 +114,22 @@ export default function EvaluatingCreate() {
const navigate = useNavigate();
const handleCreateEvaluation = async () => {
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(
@@ -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);
@@ -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]"
>
<TableCell
<TableCell
className="font-medium max-w-[200px]"
onClick={() => {
window.libname = [el.name, el.description];
@@ -520,27 +520,27 @@ const handleOpenSettings = (lib) => {
}}
>
<div className="flex items-center gap-2 py-2">
<div className="w-[50px] h-[50px]">
<img src={TipPng} alt="" />
<div className="flex items-center justify-center w-[50px] h-[50px] bg-primary text-white rounded-[10px]">
<BookCopy />
</div>
<div>
<div className="truncate max-w-[500px] w-[264px] text-[18px] font-medium pt-2 flex items-center gap-2">
{el.name}
</div>
<QuestionTooltip
content={el.description || ''}
content={el.description || ''}
error={false}
className="w-full text-start"
>
<div className="truncate max-w-[300px] text-[14px] text-[#5A5A5A] pt-1">
{el.description || ''}
{el.description || ''}
</div>
</QuestionTooltip>
</div>
</div>
</TableCell>
<TableCell
<TableCell
className="text-[#5A5A5A]"
onClick={() => {
window.libname = [el.name, el.description];
@@ -551,7 +551,7 @@ const handleOpenSettings = (lib) => {
{el.update_time.replace('T', ' ')}
</TableCell>
<TableCell
<TableCell
className="max-w-[300px] break-all"
onClick={() => {
window.libname = [el.name, el.description];
@@ -565,15 +565,15 @@ const handleOpenSettings = (lib) => {
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Select
key={`${el.id}-${modalKey}`}
key={`${el.id}-${modalKey}`}
open={selectOpenId === el.id}
onOpenChange={(isOpen) => {
onOpenChange={(isOpen) => {
setSelectOpenId(isOpen ? el.id : null);
}}
onValueChange={(selectedValue) => {
setSelectOpenId(null);
console.log("Selected value:", selectedValue, "for lib:", el.id);
switch (selectedValue) {
case 'copy':
el.state === 1 && handleCopy(el);
@@ -606,7 +606,7 @@ const handleOpenSettings = (lib) => {
<Ellipsis size={24} color="#a69ba2" strokeWidth={1.75} />
)}
</SelectTrigger>
<SelectContent
<SelectContent
onClick={(e) => {
e.stopPropagation();
}}
@@ -620,12 +620,12 @@ const handleOpenSettings = (lib) => {
>
<div className="flex gap-2 items-center">
<Copy className="w-4 h-4" />
{ t('lib.copy') }
{t('lib.copy')}
</div>
</SelectItem>
)}
<SelectItem
value="set"
<SelectItem
value="set"
showIcon={false}
>
<div className="flex gap-2 items-center">
@@ -663,7 +663,7 @@ const handleOpenSettings = (lib) => {
/>
</div>
</div>
{/* 创建弹窗 */}
<CreateModal
datalist={datalist}
@@ -672,18 +672,18 @@ const handleOpenSettings = (lib) => {
onLoadEnd={() => { }}
mode="create"
/>
{/* 编辑(设置)弹窗 - 使用 key 强制重新渲染 */}
{settingsOpen && (
<CreateModal
key={`settings-modal-${modalKey}`}
datalist={datalist}
open={settingsOpen}
onOpenChange={handleSettingsClose}
onLoadEnd={reload}
mode="edit"
currentLib={currentSettingLib}
/>
<CreateModal
key={`settings-modal-${modalKey}`}
datalist={datalist}
open={settingsOpen}
onOpenChange={handleSettingsClose}
onLoadEnd={reload}
mode="edit"
currentLib={currentSettingLib}
/>
)}
</div>
);
@@ -56,7 +56,6 @@ const FileUploadStep2 = forwardRef(({ step, resultFiles, isSubmitting, onNext, o
});
const displayMode: DisplayModeType | null = useMemo(() => {
if (!resultFiles || resultFiles.length === 0) return null;
console.log(resultFiles,55555);
const hasTableFiles = resultFiles.some(file => file.fileType === 'table');
const hasDocumentFiles = resultFiles.some(file => file.fileType === 'file');
@@ -338,6 +337,7 @@ const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule)
id: file.id,
filePath: file.file_path,
fileName: file.fileName,
previewUrl:file.previewUrl,
suffix: file.suffix,
fileType: file.fileType,
excelRule: file.fileType === 'table' ? {
@@ -1,4 +1,3 @@
import CardComponent from "@/components/bs-comp/cardComponent";
import ProgressItem from "@/components/bs-comp/knowledgeUploadComponent/ProgressItem";
import { Button } from "@/components/bs-ui/button";
@@ -15,106 +14,148 @@ export default function FileUploadStep4({ data ,kId}) {
const [finish, setFinish] = useState(true)
const navigate = useNavigate()
const { id: kid } = useParams()
console.log(data,kId,44);
console.log(data,kId,44);
const [files, setFiles] = useState([])
const timerRef = useRef(null); // 轮询定时器引用
const fileIdsRef = useRef([]); // 文件ID列表引用
// 初始化文件状态
const processingRef = useRef(new Set()); // 跟踪正在处理的文件ID
const isPollingRef = useRef(false); // 防止轮询并发
// 初始化文件状态(只执行一次)
useEffect(() => {
const initialFiles = data.map(item => ({
id: item.fileId,
fileName: item.fileName,
error: false,
reason: '',
progress: 'await' // 初始状态设为解析中
}));
setFiles(initialFiles);
fileIdsRef.current = data.map(item => item.fileId); // 保存文件ID列表
}, [data]);
if (files.length === 0 && data.length > 0) { // 防止重复初始化
const initialFiles = data.map(item => ({
id: item.fileId,
fileName: item.fileName,
error: false,
reason: '',
progress: 'await' // 初始状态设为解析中
}));
setFiles(initialFiles);
fileIdsRef.current = data.map(item => item.fileId);
// 初始化处理中的文件集合
fileIdsRef.current.forEach(id => processingRef.current.add(id));
}
}, [data]); // 只依赖data变化
// 轮询文件状态
useEffect(() => {
// 如果文件列表为空,直接完成
// 如果没有文件需要处理,直接完成
if (fileIdsRef.current.length === 0) {
setFinish(true);
return;
}
// 轮询函数
// 轮询函数(添加并发控制)
const pollFilesStatus = async () => {
console.log(kid,fileIdsRef.current,22);
// 防止上一次请求未完成时发起新请求
if (isPollingRef.current) return;
isPollingRef.current = true;
try {
// 只轮询仍在处理中的文件
const pendingFileIds = Array.from(processingRef.current);
if (pendingFileIds.length === 0) {
clearInterval(timerRef.current);
setFinish(true);
return;
}
console.log(kid,kId, pendingFileIds, 22);
const res = await readFileByLibDatabase({
id: kid || kId,
page: 0,
pageSize: 0,
file_ids: fileIdsRef.current
file_ids: pendingFileIds // 只查询未完成的文件
});
// 更新文件状态
setFiles(prev => {
const updatedFiles = [...prev];
const resMap = new Map(res.data.map(item => [item.id, item]));
return prev.map(file => {
if (resMap.has(file.id)) {
const resItem = resMap.get(file.id);
let progress = 'await';
let error = false;
let reason = '';
pendingFileIds.forEach(fileId => {
if (resMap.has(fileId)) {
const resItem = resMap.get(fileId);
const fileIndex = updatedFiles.findIndex(f => f.id === fileId);
if (fileIndex !== -1) {
let progress = updatedFiles[fileIndex].progress;
let error = updatedFiles[fileIndex].error;
let reason = updatedFiles[fileIndex].reason;
if (resItem.status === 2) {
progress = 'end'; // 成功
} else if (resItem.status === 3) {
progress = 'end'; // 失败
error = true;
reason = resItem.remark;
// 状态变为完成或失败时,从处理中集合移除
if (resItem.status === 2) {
progress = 'end'; // 成功
processingRef.current.delete(fileId);
} else if (resItem.status === 3) {
progress = 'end'; // 失败
error = true;
reason = resItem.remark;
processingRef.current.delete(fileId);
}
updatedFiles[fileIndex] = {
...updatedFiles[fileIndex],
progress,
error,
reason
};
}
return { ...file, progress, error, reason };
}
return file;
});
return updatedFiles;
});
} catch (e) {
console.error("轮询文件状态出错:", e);
} finally {
isPollingRef.current = false; // 释放锁
}
};
// 立即执行第一次轮询,然后每5秒轮询一次
pollFilesStatus();
timerRef.current = setInterval(pollFilesStatus, 3000 + fileIdsRef.current.length * 10);
timerRef.current = setInterval(pollFilesStatus, 5000); // 固定间隔,避免过频繁
// 清理定时器
return () => clearInterval(timerRef.current);
}, [kid]);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, [kid, kId]); // 只依赖kid和kId变化
// 检查所有文件是否完成
useEffect(() => {
// 检查是否有文件仍在处理中
const hasProcessingFiles = files.some(f => f.progress !== 'end');
// 只有当存在文件且没有文件在处理中时,才标记为完成
if (files.length > 0 && !hasProcessingFiles) {
console.log('所有文件处理完成');
clearInterval(timerRef.current);
setFinish(true);
} else {
// 仍有文件在处理中或文件列表为空
if (files.length > 0) {
const processingCount = files.filter(f => f.progress !== 'end').length;
console.log(`仍有${processingCount}个文件正在处理中`);
useEffect(() => {
// 当处理中集合为空时,标记为完成
if (processingRef.current.size === 0 && fileIdsRef.current.length > 0) {
console.log('所有文件处理完成');
if (timerRef.current) {
clearInterval(timerRef.current);
}
setFinish(true);
} else {
setFinish(false);
}
setFinish(false);
}
}, [files]);
console.log('fukes :>> ', data, files);
}, [files]); // 依赖文件状态变化
const [details] = useKnowledgeDetails([kid])
console.log('files :>> ', files);
let finalId = kid;
if (kId) {
finalId = kId.replace(/\D/g, '');
}
const [details] = useKnowledgeDetails([finalId])
const handleCreateFlow = async (params) => {
const model = await getLlmDefaultModel()
const flow = await getKnowledgeDefaultFlowTemplate(kid, details[0].name, model.model_id)
console.log(details,9999);
const flow = await getKnowledgeDefaultFlowTemplate(finalId, details[0]?.name || '', model.model_id)
const res = await captureAndAlertRequestErrorHoc(createWorkflowApi(
"文档知识库问答-" + generateUUID(5),
"检索文档知识库,根据检索结果进行回答。",
@@ -148,8 +189,7 @@ useEffect(() => {
</div>
};
// 保持getKnowledgeDefaultFlowTemplate函数不变
const getKnowledgeDefaultFlowTemplate = async (kid, kname, modelId) => {
const templates = await getWorkflowNodeTemplate()
let startNode = null
@@ -289,4 +329,5 @@ const getKnowledgeDefaultFlowTemplate = async (kid, kname, modelId) => {
}
}
}
}
}
@@ -55,7 +55,7 @@ export default function Paragraphs({ fileId, onBack }) {
const searchInputRef = useRef(null);
const isChangingRef = useRef(false);
const [isInitReady, setIsInitReady] = useState(false);
const [previewUrl,setPreviewUrl] = useState()
// 1. 修复:URL乱码(添加decodeURIComponent)+ 确保数据顺序
const fetchFileUrl = useCallback(async (fileId) => {
console.log('获取文件URL:', fileId);
@@ -66,15 +66,20 @@ export default function Paragraphs({ fileId, onBack }) {
const res = await getFilePathApi(fileId);
const pares = await getFileBboxApi(fileId)
setPartitions(pares)
console.log('getFilePathApi 响应:', res,pares);
console.log('getFilePathApi 响应:', res.original_url,pares);
// 修复:提取URL并解码(解决中文/特殊字符乱码)
let url;
if (res.data) {
url = res.data.url || res.data.filePath || res.data;
if (res) {
url = res.original_url || res.data.filePath || res.url;
if(res.preview_url){
setPreviewUrl(res.preview_url)
}
} else {
url = res.url || res.filePath || res;
}
console.log(url,2222);
const trimmedUrl = (url || '').trim();
if (!trimmedUrl) {
@@ -363,6 +368,7 @@ export default function Paragraphs({ fileId, onBack }) {
skipToStep: 2,
fileId: selectedFileId,
fileData: {
previewUrl:previewUrl,
id: currentFile?.id,
name: currentFile?.name,
split_rule: currentFile?.split_rule || currentFile?.fullData?.split_rule,
@@ -45,8 +45,9 @@ export default function PreviewFile({
const targetFile = matchedRawFile || file;
const fileParseType = targetFile.parse_type;
const fileName = targetFile.name || file.fileName;
const fileName = targetFile.name || file.fileName||file.name;
const suffix = useMemo(() => {
console.log(fileName,3322);
return fileName?.split('.').pop()?.toLowerCase() || '';
}, [fileName]);
@@ -29,6 +29,8 @@ export type Partition = {
[key in string]: { text: string, type: string, part_id: string }
}
export default function PreviewResult({showPreview ,previewCount, rules, step, applyEachCell, cellGeneralConfig,kId,handlePreviewResult }: IProps) {
console.log(showPreview,rules,567);
const { id } = useParams()
const [chunks, setChunks] = useState([]) // 当前文件分块
@@ -71,13 +73,22 @@ export default function PreviewResult({showPreview ,previewCount, rules, step, a
const { fileList, pageHeaderFooter, chunkOverlap, chunkSize, enableFormula, forceOcr, knowledgeId
, retainImages, separator, separatorRule } = rules
const currentFile = fileList.find(file => file.id === selectId)
let preview_url
if(showPreview){
preview_url = currentFile.previewUrl ||currentFile?.filePath
}else {
preview_url=currentFile?.filePath
}
console.log(preview_url ,currentFile,6789);
captureAndAlertRequestErrorHoc(previewFileSplitApi({
// 缓存(修改规则后需要清空缓存, 切换文件使用缓存)
// previewCount变更时为重新预览分段操作,不使用缓存
cache: prevPreviewCountMapRef.current[currentFile.id] === previewCount,
knowledge_id: id||kId,
file_list: [{
file_path: currentFile?.filePath,
file_path:preview_url,
excel_rule: applyEachCell
? currentFile.excelRule
: { ...cellGeneralConfig }
@@ -151,7 +162,7 @@ export default function PreviewResult({showPreview ,previewCount, rules, step, a
setChunks={setChunks}
partitions={partitions}
/>}
<div className={cn('relative', 'w-1/2')}>
<div className={cn('relative',)}>
{/* 下拉框 - 右上角 */}
{(step === 3 ||(step===2 &&showPreview))&&(
<div className="flex justify-end">
@@ -251,14 +251,29 @@ export default function QasPage() {
}
}, [datalist])
useEffect(() => {
// @ts-ignore
const libname = window.libname // 临时记忆
if (libname) {
localStorage.setItem('libname', window.libname)
// 修改 useEffect 中处理标题的部分
useEffect(() => {
// 处理 window.libname 可能的格式问题
let libName = '';
// 检查 window.libname 是否存在且有效
if (window.libname) {
// 处理数组格式([名称, 描述])
if (Array.isArray(window.libname) && window.libname.length > 0) {
libName = window.libname[0];
}
// 处理字符串格式
else if (typeof window.libname === 'string') {
libName = window.libname;
}
setTitle(window.libname[0] || localStorage.getItem('libname'))
}, [])
// 存储到 localStorage 时只存名称
localStorage.setItem('libname', libName);
}
// 从 localStorage 获取备份
else {
libName = localStorage.getItem('libname') || '';
}
setTitle(libName || t('unknownKnowledgeBase')); // 提供默认文本
}, []);
const handleEnableSelected = async () => {
if (!selectedItems.length) return;
@@ -1,14 +1,16 @@
import AppAvator from "@/components/bs-comp/cardComponent/avatar";
import MessagePanne from "@/components/bs-comp/chatComponent/MessagePanne";
import { useMessageStore } from "@/components/bs-comp/chatComponent/messageStore";
import { LoadingIcon } from "@/components/bs-icons/loading";
import { Button } from "@/components/bs-ui/button";
import ShadTooltip from "@/components/ShadTooltipComponent";
import { getDeleteFlowApi } from "@/controllers/API/flow";
import ChatMessages from "@/pages/BuildPage/flow/FlowChat/ChatMessages";
import { useMessageStore as useFlowMessageStore } from "@/pages/BuildPage/flow/FlowChat/messageStore";
import { useAssistantStore } from "@/store/assistantStore";
import { AppNumType } from "@/types/app";
import { ArrowLeft } from "lucide-react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
@@ -47,6 +49,8 @@ export default function AppChatDetail() {
}
}, [])
const logo = useAppAvatar(cid, type)
return <div>
{loading && <div className="absolute w-full h-full top-0 left-0 flex justify-center items-center z-10 bg-[rgba(255,255,255,0.6)] dark:bg-blur-shared">
<LoadingIcon />
@@ -66,8 +70,8 @@ export default function AppChatDetail() {
</div>
<div className="h-[calc(100vh-132px)]">
{type === AppNumType.FLOW
? <ChatMessages logo={''} debug useName={''} guideWord={''} loadMore={() => loadMoreFlowHistoryMsg(fid, true)} onMarkClick={null}></ChatMessages>
: <MessagePanne logo='' useName='' guideWord=''
? <ChatMessages logo={logo} debug useName={''} guideWord={''} loadMore={() => loadMoreFlowHistoryMsg(fid, true)} onMarkClick={null}></ChatMessages>
: <MessagePanne logo={logo} useName='' guideWord=''
loadMore={() => loadMoreHistoryMsg(fid, true)}
></MessagePanne>
}
@@ -75,3 +79,19 @@ export default function AppChatDetail() {
</div>
</div >
};
const useAppAvatar = (cid, flowType) => {
const [logo, setLogo] = useState('')
const [title, setTitle] = useState('')
const loadAppDetail = async () => {
const lostflow = await getDeleteFlowApi(cid)
setLogo(lostflow.flow_logo)
setTitle(lostflow.flow_name)
}
useEffect(() => {
loadAppDetail()
}, [])
return <AppAvator id={title} flowType={flowType} url={logo} className="size-5 min-w-5"></AppAvator>
}