From 548ba0ae369516b55883a864dbc231067ceef262 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 17 Mar 2026 17:03:05 +0800 Subject: [PATCH 01/24] fix(web): file download --- .../components/PerceptualLastInfo.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx index 62489f2f..b3bbd7ba 100644 --- a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx +++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 18:32:23 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 18:32:23 + * @Last Modified time: 2026-03-17 17:02:46 */ import { type FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -90,10 +90,10 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }) } - const handleDownload = () => { - if (!data.file_path) return - window.open(data.file_path, '_blank') - } + // const handleDownload = () => { + // if (!data.file_path) return + // window.open(data.file_path, '_blank') + // } return ( ) : ( -
{data.file_name}
+ {data.file_name} ) ) : (
{t('empty.tableEmpty')}
From ec5cb42f676f3f8abcd85cbc18440c09e744ef3e Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 17 Mar 2026 17:16:01 +0800 Subject: [PATCH 02/24] fix(web): file download --- .../components/PerceptualLastInfo.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx index b3bbd7ba..1af50b3e 100644 --- a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx +++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 18:32:23 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 17:02:46 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-17 17:15:14 */ import { type FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -90,10 +90,25 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }) } - // const handleDownload = () => { - // if (!data.file_path) return - // window.open(data.file_path, '_blank') - // } + const handleDownload = async () => { + if (!data.file_path) return + if (data.file_path.includes('.redbearai.') || data.file_path.includes('.memorybear.')) { + try { + const res = await fetch(data.file_path) + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = data.file_name || 'download' + a.click() + URL.revokeObjectURL(url) + } catch { + window.open(data.file_path, '_blank') + } + } else { + window.open(data.file_path, '_blank') + } + } return ( ) : ( - {data.file_name} +
{data.file_name}
) ) : (
{t('empty.tableEmpty')}
From 262a9ddc4853f7d9a0a74da98c5da3f9271c0d8e Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Tue, 17 Mar 2026 10:30:09 +0800 Subject: [PATCH 03/24] fix(multimodel): filter unsupported files during perception memory write --- api/app/services/memory_perceptual_service.py | 12 +-- api/app/services/multimodal_service.py | 100 +++++++++--------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/api/app/services/memory_perceptual_service.py b/api/app/services/memory_perceptual_service.py index 53d935fe..580a8857 100644 --- a/api/app/services/memory_perceptual_service.py +++ b/api/app/services/memory_perceptual_service.py @@ -262,17 +262,17 @@ class MemoryPerceptualService: } if file_type in [FileType.IMAGE, FileType.VIDEO]: file_modalities = { - "scene": content.get("scene") + "scene": content.get("scene", []) } elif file_type in [FileType.DOCUMENT]: file_modalities = { - "section_count": content.get("section_count"), - "title": content.get("title"), - "first_line": content.get("first_line") + "section_count": content.get("section_count", 0), + "title": content.get("title", ""), + "first_line": content.get("first_line", "") } else: file_modalities = { - "speaker_count": content.get("speaker_count") + "speaker_count": content.get("speaker_count", 0) } self.repository.create_perceptual_memory( end_user_id=uuid.UUID(end_user_id), @@ -280,7 +280,7 @@ class MemoryPerceptualService: file_path=file_url, file_name=filename, file_ext=file_ext, - summary=content.get('summary'), + summary=content.get('summary', ""), meta_data={ "content": file_content, "modalities": file_modalities diff --git a/api/app/services/multimodal_service.py b/api/app/services/multimodal_service.py index 208f6ec0..3695c56f 100644 --- a/api/app/services/multimodal_service.py +++ b/api/app/services/multimodal_service.py @@ -48,22 +48,22 @@ class MultimodalFormatStrategy(ABC): self.file = file @abstractmethod - async def format_image(self, url: str, content: bytes | None = None) -> Dict[str, Any]: + async def format_image(self, url: str, content: bytes | None = None) -> tuple[bool, Dict[str, Any]]: """格式化图片""" pass @abstractmethod - async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + async def format_document(self, file_name: str, text: str) -> tuple[bool, Dict[str, Any]]: """格式化文档""" pass @abstractmethod - async def format_audio(self, file_type: str, url: str, content: bytes | None = None) -> Dict[str, Any]: + async def format_audio(self, file_type: str, url: str, content: bytes | None = None) -> tuple[bool, Dict[str, Any]]: """格式化音频""" pass @abstractmethod - async def format_video(self, url: str) -> Dict[str, Any]: + async def format_video(self, url: str) -> tuple[bool, Dict[str, Any]]: """格式化视频""" pass @@ -71,16 +71,16 @@ class MultimodalFormatStrategy(ABC): class DashScopeFormatStrategy(MultimodalFormatStrategy): """通义千问策略""" - async def format_image(self, url: str, content: bytes | None = None) -> Dict[str, Any]: + async def format_image(self, url: str, content: bytes | None = None) -> tuple[bool, Dict[str, Any]]: """通义千问图片格式:{"type": "image", "image": "url"}""" - return { + return True, { "type": "image", "image": url } - async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + async def format_document(self, file_name: str, text: str) -> tuple[bool, Dict[str, Any]]: """通义千问文档格式""" - return { + return True, { "type": "text", "text": f"\n{text}\n" } @@ -91,26 +91,26 @@ class DashScopeFormatStrategy(MultimodalFormatStrategy): url: str, content: bytes | None = None, transcription: Optional[str] = None - ) -> Dict[str, Any]: + ) -> tuple[bool, Dict[str, Any]]: """ 通义千问音频格式 - 原生支持: qwen-audio 系列 - 其他模型: 需要转录为文本 """ if transcription: - return { + return True, { "type": "text", "text": f"" } # 通义千问音频格式:{"type": "audio", "audio": "url"} - return { + return True, { "type": "audio", "audio": url } - async def format_video(self, url: str) -> Dict[str, Any]: + async def format_video(self, url: str) -> tuple[bool, Dict[str, Any]]: """通义千问视频格式(qwen-vl 系列原生支持)""" - return { + return True, { "type": "video", "video": url } @@ -119,7 +119,7 @@ class DashScopeFormatStrategy(MultimodalFormatStrategy): class BedrockFormatStrategy(MultimodalFormatStrategy): """Bedrock/Anthropic 策略""" - async def format_image(self, url: str, content: bytes | None = None) -> Dict[str, Any]: + async def format_image(self, url: str, content: bytes | None = None) -> tuple[bool, Dict[str, Any]]: """ Bedrock/Anthropic 格式: base64 编码 {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} @@ -142,7 +142,7 @@ class BedrockFormatStrategy(MultimodalFormatStrategy): logger.info(f"图片编码完成: media_type={media_type}, size={len(base64_data)}") - return { + return True, { "type": "image", "source": { "type": "base64", @@ -151,13 +151,13 @@ class BedrockFormatStrategy(MultimodalFormatStrategy): } } - async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + async def format_document(self, file_name: str, text: str) -> tuple[bool, Dict[str, Any]]: """Bedrock/Anthropic 文档格式(需要 base64 编码)""" # Bedrock 文档需要 base64 编码 text_bytes = text.encode('utf-8') base64_text = base64.b64encode(text_bytes).decode('utf-8') - return { + return True, { "type": "document", "source": { "type": "base64", @@ -171,24 +171,24 @@ class BedrockFormatStrategy(MultimodalFormatStrategy): url: str, content: bytes | None = None, transcription: Optional[str] = None - ) -> Dict[str, Any]: + ) -> tuple[bool, Dict[str, Any]]: """ Bedrock/Anthropic 音频格式 不支持原生音频,必须转录为文本 """ if transcription: - return { + return True, { "type": "text", "text": f"[音频转录]\n{transcription}" } - return { + return False, { "type": "text", "text": "[音频文件:Bedrock 不支持原生音频,请启用音频转文本功能]" } - async def format_video(self, url: str) -> Dict[str, Any]: + async def format_video(self, url: str) -> tuple[bool, Dict[str, Any]]: """Bedrock/Anthropic 视频格式""" - return { + return False, { "type": "text", "text": f"" } @@ -197,18 +197,18 @@ class BedrockFormatStrategy(MultimodalFormatStrategy): class OpenAIFormatStrategy(MultimodalFormatStrategy): """OpenAI 策略""" - async def format_image(self, url: str, content: bytes | None = None) -> Dict[str, Any]: + async def format_image(self, url: str, content: bytes | None = None) -> tuple[bool, Dict[str, Any]]: """OpenAI 格式: {"type": "image_url", "image_url": {"url": "..."}}""" - return { + return True, { "type": "image_url", "image_url": { "url": url } } - async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + async def format_document(self, file_name: str, text: str) -> tuple[bool, Dict[str, Any]]: """OpenAI 文档格式""" - return { + return True, { "type": "text", "text": f"\n{text}\n" } @@ -219,14 +219,14 @@ class OpenAIFormatStrategy(MultimodalFormatStrategy): url: str, content: bytes | None = None, transcription: Optional[str] = None - ) -> Dict[str, Any]: + ) -> tuple[bool, Dict[str, Any]]: """ OpenAI 音频格式 - gpt-4o-audio 系列支持原生音频(需要 base64 编码) - 其他模型使用转录文本 """ if transcription: - return { + return True, { "type": "text", "text": f"" } @@ -255,7 +255,7 @@ class OpenAIFormatStrategy(MultimodalFormatStrategy): # supported_ext = {"wav", "mp3", "mp4", "ogg", "flac", "webm", "m4a", "wave", "x-m4a"} file_ext = "wav" if not file_ext else file_ext - return { + return True, { "type": "input_audio", "input_audio": { "data": f"data:;base64,{base64_audio}", @@ -264,14 +264,14 @@ class OpenAIFormatStrategy(MultimodalFormatStrategy): } except Exception as e: logger.error(f"下载音频失败: {e}") - return { + return False, { "type": "text", "text": f"[音频处理失败: {str(e)}]" } - async def format_video(self, url: str) -> Dict[str, Any]: + async def format_video(self, url: str) -> tuple[bool, Dict[str, Any]]: """OpenAI 视频格式""" - return { + return True, { "type": "video_url", "video_url": { "url": url @@ -366,21 +366,25 @@ class MultimodalService: file.url = await self.get_file_url(file) try: if file.type == FileType.IMAGE and "vision" in self.capability: - content = await self._process_image(file, strategy) + is_support, content = await self._process_image(file, strategy) result.append(content) - self.write_perceptual_memory(end_user_id, file.type, file.url, content) + if is_support: + self.write_perceptual_memory(end_user_id, file.type, file.url, content) elif file.type == FileType.DOCUMENT: - content = await self._process_document(file, strategy) + is_support, content = await self._process_document(file, strategy) result.append(content) - self.write_perceptual_memory(end_user_id, file.type, file.url, content) + if is_support: + self.write_perceptual_memory(end_user_id, file.type, file.url, content) elif file.type == FileType.AUDIO and "audio" in self.capability: - content = await self._process_audio(file, strategy) + is_support, content = await self._process_audio(file, strategy) result.append(content) - self.write_perceptual_memory(end_user_id, file.type, file.url, content) + if is_support: + self.write_perceptual_memory(end_user_id, file.type, file.url, content) elif file.type == FileType.VIDEO and "video" in self.capability: - content = await self._process_video(file, strategy) + is_support, content = await self._process_video(file, strategy) result.append(content) - self.write_perceptual_memory(end_user_id, file.type, file.url, content) + if is_support: + self.write_perceptual_memory(end_user_id, file.type, file.url, content) else: logger.warning(f"不支持的文件类型: {file.type}") except Exception as e: @@ -413,7 +417,7 @@ class MultimodalService: if end_user_id and self.api_config: write_perceptual_memory.delay(end_user_id, self.api_config.model_dump(), file_type, file_url, file_message) - async def _process_image(self, file: FileInput, strategy) -> Dict[str, Any]: + async def _process_image(self, file: FileInput, strategy) -> tuple[bool, Dict[str, Any]]: """ 处理图片文件 @@ -429,12 +433,12 @@ class MultimodalService: return await strategy.format_image(file.url, content=file.get_content()) except Exception as e: logger.error(f"处理图片失败: {e}", exc_info=True) - return { + return False, { "type": "text", "text": f"[图片处理失败: {str(e)}]" } - async def _process_document(self, file: FileInput, strategy) -> Dict[str, Any]: + async def _process_document(self, file: FileInput, strategy) -> tuple[bool, Dict[str, Any]]: """ 处理文档文件(PDF、Word 等) @@ -446,7 +450,7 @@ class MultimodalService: Dict: 根据 provider 返回不同格式的文档内容 """ if file.transfer_method == TransferMethod.REMOTE_URL: - return { + return True, { "type": "text", "text": f"\n{await self._extract_document_text(file)}\n" } @@ -464,7 +468,7 @@ class MultimodalService: # 使用策略格式化文档 return await strategy.format_document(file_name, text) - async def _process_audio(self, file: FileInput, strategy) -> Dict[str, Any]: + async def _process_audio(self, file: FileInput, strategy) -> tuple[bool, Dict[str, Any]]: """ 处理音频文件 @@ -492,12 +496,12 @@ class MultimodalService: return await strategy.format_audio(file.file_type, file.url, file.get_content(), transcription) except Exception as e: logger.error(f"处理音频失败: {e}", exc_info=True) - return { + return False, { "type": "text", "text": f"[音频处理失败: {str(e)}]" } - async def _process_video(self, file: FileInput, strategy) -> Dict[str, Any]: + async def _process_video(self, file: FileInput, strategy) -> tuple[bool, Dict[str, Any]]: """ 处理视频文件 @@ -513,7 +517,7 @@ class MultimodalService: return await strategy.format_video(file.url) except Exception as e: logger.error(f"处理视频失败: {e}", exc_info=True) - return { + return False, { "type": "text", "text": f"[视频处理失败: {str(e)}]" } From 8ddacb7bc90b760e602bb38a160838d96e3cd69e Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Tue, 17 Mar 2026 17:24:02 +0800 Subject: [PATCH 04/24] fix(perceptual): resolve inconsistency between local filename and actual filename --- api/app/services/memory_perceptual_service.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/app/services/memory_perceptual_service.py b/api/app/services/memory_perceptual_service.py index 580a8857..8a7c86e2 100644 --- a/api/app/services/memory_perceptual_service.py +++ b/api/app/services/memory_perceptual_service.py @@ -5,12 +5,14 @@ from urllib.parse import urlparse, unquote import json_repair from jinja2 import Template +from sqlalchemy import select from sqlalchemy.orm import Session from app.core.error_codes import BizCode from app.core.exceptions import BusinessException from app.core.logging_config import get_business_logger from app.core.models import RedBearLLM, RedBearModelConfig +from app.models import FileMetadata from app.models.memory_perceptual_model import PerceptualType, FileStorageService from app.models.prompt_optimizer_model import RoleType from app.repositories.memory_perceptual_repository import MemoryPerceptualRepository @@ -245,6 +247,18 @@ class MemoryPerceptualService: filename = os.path.basename(path) filename = unquote(filename) file_ext = os.path.splitext(filename)[1] + try: + file_id = uuid.UUID(filename) + stmt = select(FileMetadata).where( + FileMetadata.id == file_id + ) + file = self.db.execute(stmt).scalar_one_or_none() + + if file: + filename = file.file_name + file_ext = file.file_ext + except ValueError: + business_logger.debug(f"Remote file, file_id={filename}") if not file_ext: if file_type == FileType.AUDIO: file_ext = ".mp3" From 774719fb50a38031016ed7a5c718ae6f0cc0a35c Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 17 Mar 2026 17:37:03 +0800 Subject: [PATCH 05/24] revert(web): file download --- .../components/PerceptualLastInfo.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx index 1af50b3e..ad7946f0 100644 --- a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx +++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 18:32:23 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 17:15:14 + * @Last Modified time: 2026-03-17 17:36:49 */ import { type FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -92,22 +92,7 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' const handleDownload = async () => { if (!data.file_path) return - if (data.file_path.includes('.redbearai.') || data.file_path.includes('.memorybear.')) { - try { - const res = await fetch(data.file_path) - const blob = await res.blob() - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = data.file_name || 'download' - a.click() - URL.revokeObjectURL(url) - } catch { - window.open(data.file_path, '_blank') - } - } else { - window.open(data.file_path, '_blank') - } + window.open(data.file_path, '_blank') } return ( From 3b8a806661159080509606bb471d6facce6b024c Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Tue, 17 Mar 2026 18:01:28 +0800 Subject: [PATCH 06/24] feat(workflow): expose workflow memory enable status in app share config API --- api/app/controllers/public_share_controller.py | 1 + api/app/services/workflow_service.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/api/app/controllers/public_share_controller.py b/api/app/controllers/public_share_controller.py index 19c82790..b8fec55d 100644 --- a/api/app/controllers/public_share_controller.py +++ b/api/app/controllers/public_share_controller.py @@ -661,6 +661,7 @@ async def config_query( content = { "app_type": release.app.type, "variables": workflow_service.get_start_node_variables(release.config), + "memory": workflow_service.is_memory_enable(release.config), "features": release.config.get("features") } elif release.app.type == AppType.AGENT: diff --git a/api/app/services/workflow_service.py b/api/app/services/workflow_service.py index 4e7268d3..7aca3c2f 100644 --- a/api/app/services/workflow_service.py +++ b/api/app/services/workflow_service.py @@ -868,6 +868,14 @@ class WorkflowService: return node.get("config", {}).get("variables", []) raise BusinessException("workflow config error - start node not found") + @staticmethod + def is_memory_enable(config: dict) -> bool: + nodes = config.get("nodes", []) + for node in nodes: + if node.get("type") in [NodeType.MEMORY_READ, NodeType.MEMORY_WRITE]: + return True + return False + # ==================== 依赖注入函数 ==================== From 71b3b665b5718b9ea04f50dde7ac4b7fd894d00d Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 17 Mar 2026 18:14:19 +0800 Subject: [PATCH 07/24] fix(web): max_file_count precision --- .../components/FeaturesConfig/FileUploadSettingModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx index 6db26478..3579497a 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-05 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-16 18:36:09 + * @Last Modified time: 2026-03-17 18:10:47 */ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd'; @@ -128,7 +128,7 @@ const FileUploadSettingModal = forwardRef{t('application.maxCount')} - + From 599ccb6bdeb3da66868340c7f4146301f4cfa548 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 17 Mar 2026 18:41:27 +0800 Subject: [PATCH 08/24] fix(web): audio recorder add max size check --- web/src/components/AudioRecorder/index.tsx | 16 ++++++++++++++-- web/src/components/Chat/ChatToolbar.tsx | 7 +++---- web/src/views/Conversation/index.tsx | 4 ++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/web/src/components/AudioRecorder/index.tsx b/web/src/components/AudioRecorder/index.tsx index b3b87130..639a9109 100644 --- a/web/src/components/AudioRecorder/index.tsx +++ b/web/src/components/AudioRecorder/index.tsx @@ -2,10 +2,12 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:11:51 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-16 18:06:00 + * @Last Modified time: 2026-03-17 18:39:09 */ import { type FC, useRef, useState } from 'react' import RecordRTC from 'recordrtc' +import { App } from 'antd' +import { useTranslation } from 'react-i18next'; import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' import { request } from '@/utils/request' @@ -20,6 +22,7 @@ interface AudioRecorderProps { /** Additional config passed to the upload request */ requestConfig?: Record; disabled?: boolean; + maxSize?: number; } const AudioRecorder: FC = ({ @@ -27,8 +30,11 @@ const AudioRecorder: FC = ({ className = '', action = fileUploadUrlWithoutApiPrefix, requestConfig = {}, - disabled = false + disabled = false, + maxSize, }) => { + const { message } = App.useApp() + const { t } = useTranslation(); // Whether the recorder is currently capturing audio const [isRecording, setIsRecording] = useState(false) // Holds the RecordRTC instance across renders @@ -57,6 +63,12 @@ const AudioRecorder: FC = ({ recorderRef.current.stopRecording(() => { const blob = recorderRef.current!.getBlob() const url = recorderRef.current!.toURL() + + if (maxSize && blob.size > maxSize * 1024 * 1024) { + message.error(t('common.fileSizeTip', { size: maxSize })); + return + } + const formData = new FormData() formData.append('file', blob, `recording_${Date.now()}.webm`) request diff --git a/web/src/components/Chat/ChatToolbar.tsx b/web/src/components/Chat/ChatToolbar.tsx index 1d368c30..6a316bd5 100644 --- a/web/src/components/Chat/ChatToolbar.tsx +++ b/web/src/components/Chat/ChatToolbar.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-03-17 14:22:25 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 14:22:25 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-17 18:39:49 */ // Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react' @@ -151,8 +151,6 @@ const ChatToolbar = forwardRef(({ }) } - console.log('queryValues', queryValues) - return (
@@ -183,6 +181,7 @@ const ChatToolbar = forwardRef(({ action={uploadAction} requestConfig={uploadRequestConfig} onRecordingComplete={handleRecordingComplete} + maxSize={file_upload?.audio_max_size_mb} /> diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index 3e4833de..7a57c615 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:58:03 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 15:39:17 + * @Last Modified time: 2026-03-17 18:30:58 */ /** * Conversation Page @@ -369,7 +369,7 @@ const Conversation: FC = () => { }} extra={ <> - {features.web_search?.enabled && + {features?.web_search?.enabled && Date: Wed, 18 Mar 2026 10:46:55 +0800 Subject: [PATCH 09/24] fix(app): 1.The end users are still bound to the app. 2. Multi-modal file support includes xlsx, csv, and json. 3. The file routing protocol is consistent with the page routing. --- api/app/controllers/app_controller.py | 2 + .../controllers/file_storage_controller.py | 23 +++++++- .../controllers/public_share_controller.py | 2 + .../controllers/service/app_api_controller.py | 1 + api/app/repositories/end_user_repository.py | 8 ++- api/app/services/multimodal_service.py | 54 ++++++++++++++++++- 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 76fc0db5..d6c71f37 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -537,6 +537,7 @@ async def draft_run( # 先获取 app 的 workspace_id end_user_repo = EndUserRepository(db) new_end_user = end_user_repo.get_or_create_end_user( + app_id=app_id, workspace_id=app.workspace_id, other_id=str(current_user.id), ) @@ -869,6 +870,7 @@ async def draft_run_compare( # 先获取 app 的 workspace_id end_user_repo = EndUserRepository(db) new_end_user = end_user_repo.get_or_create_end_user( + app_id=app_id, workspace_id=app.workspace_id, other_id=str(current_user.id), ) diff --git a/api/app/controllers/file_storage_controller.py b/api/app/controllers/file_storage_controller.py index b79035c0..ff284f39 100644 --- a/api/app/controllers/file_storage_controller.py +++ b/api/app/controllers/file_storage_controller.py @@ -15,7 +15,7 @@ import os import uuid from typing import Any -from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status +from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status from fastapi.responses import FileResponse, RedirectResponse from sqlalchemy.orm import Session @@ -47,6 +47,19 @@ router = APIRouter( ) +def _match_scheme(request: Request, url: str) -> str: + """ + 将 presigned URL 的协议替换为与当前请求一致的协议(http/https)。 + 解决反向代理场景下 presigned URL 协议与请求协议不匹配的问题。 + """ + incoming_scheme = request.headers.get("x-forwarded-proto") or request.url.scheme + if url.startswith("http://") and incoming_scheme == "https": + return "https://" + url[7:] + if url.startswith("https://") and incoming_scheme == "http": + return "http://" + url[8:] + return url + + @router.post("/files", response_model=ApiResponse) async def upload_file( file: UploadFile = File(...), @@ -280,6 +293,7 @@ async def upload_file_with_share_token( @router.get("/files/{file_id}", response_model=Any) async def download_file( + request: Request, file_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -327,6 +341,7 @@ async def download_file( else: try: presigned_url = await storage_service.get_file_url(file_key, expires=3600) + presigned_url = _match_scheme(request, presigned_url) api_logger.info(f"Redirecting to presigned URL: file_key={file_key}") return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND) except FileNotFoundError: @@ -400,6 +415,7 @@ async def delete_file( @router.get("/files/{file_id}/url", response_model=ApiResponse) async def get_file_url( + request: Request, file_id: uuid.UUID, expires: int = None, permanent: bool = False, @@ -463,6 +479,7 @@ async def get_file_url( else: # For remote storage (OSS/S3), get presigned URL url = await storage_service.get_file_url(file_key, expires=expires) + url = _match_scheme(request, url) api_logger.info(f"Generated file URL: file_id={file_id}") return success( @@ -484,6 +501,7 @@ async def get_file_url( @router.get("/public/{file_id}", response_model=Any) async def public_download_file( + request: Request, file_id: uuid.UUID, expires: int = 0, signature: str = "", @@ -555,6 +573,7 @@ async def public_download_file( # For remote storage, redirect to presigned URL try: presigned_url = await storage_service.get_file_url(file_key, expires=3600) + presigned_url = _match_scheme(request, presigned_url) return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND) except Exception as e: api_logger.error(f"Failed to get presigned URL: {e}") @@ -566,6 +585,7 @@ async def public_download_file( @router.get("/permanent/{file_id}", response_model=Any) async def permanent_download_file( + request: Request, file_id: uuid.UUID, db: Session = Depends(get_db), storage_service: FileStorageService = Depends(get_file_storage_service), @@ -625,6 +645,7 @@ async def permanent_download_file( try: # Use a very long expiration (7 days max for most cloud providers) presigned_url = await storage_service.get_file_url(file_key, expires=604800) + presigned_url = _match_scheme(request, presigned_url) return RedirectResponse(url=presigned_url, status_code=status.HTTP_302_FOUND) except Exception as e: api_logger.error(f"Failed to get presigned URL: {e}") diff --git a/api/app/controllers/public_share_controller.py b/api/app/controllers/public_share_controller.py index 19c82790..0e666898 100644 --- a/api/app/controllers/public_share_controller.py +++ b/api/app/controllers/public_share_controller.py @@ -219,6 +219,7 @@ def list_conversations( app_service = AppService(db) app = app_service._get_app_or_404(share.app_id) new_end_user = end_user_repo.get_or_create_end_user( + app_id=share.app_id, workspace_id=app.workspace_id, other_id=other_id ) @@ -315,6 +316,7 @@ async def chat( app = app_service._get_app_or_404(share.app_id) workspace_id = app.workspace_id new_end_user = end_user_repo.get_or_create_end_user( + app_id=share.app_id, workspace_id=workspace_id, other_id=other_id, original_user_id=user_id diff --git a/api/app/controllers/service/app_api_controller.py b/api/app/controllers/service/app_api_controller.py index d642861e..3b054d2a 100644 --- a/api/app/controllers/service/app_api_controller.py +++ b/api/app/controllers/service/app_api_controller.py @@ -94,6 +94,7 @@ async def chat( workspace_id = app.workspace_id end_user_repo = EndUserRepository(db) new_end_user = end_user_repo.get_or_create_end_user( + app_id=app.id, workspace_id=workspace_id, other_id=other_id, ) diff --git a/api/app/repositories/end_user_repository.py b/api/app/repositories/end_user_repository.py index 590655a8..71c93634 100644 --- a/api/app/repositories/end_user_repository.py +++ b/api/app/repositories/end_user_repository.py @@ -66,7 +66,8 @@ class EndUserRepository: raise def get_or_create_end_user( - self, + self, + app_id: uuid.UUID, workspace_id: uuid.UUID, other_id: str, original_user_id: Optional[str] = None @@ -74,6 +75,7 @@ class EndUserRepository: """获取或创建终端用户 Args: + app_id: 应用ID workspace_id: 工作空间ID other_id: 第三方ID original_user_id: 原始用户ID (存储到 other_id) @@ -92,10 +94,14 @@ class EndUserRepository: if end_user: db_logger.debug(f"找到现有终端用户: 应用ID {workspace_id}、第三方ID {other_id}") + end_user.app_id=app_id + self.db.commit() + self.db.refresh(end_user) return end_user # 创建新用户 end_user = EndUser( + app_id=app_id, workspace_id=workspace_id, other_id=other_id ) diff --git a/api/app/services/multimodal_service.py b/api/app/services/multimodal_service.py index 208f6ec0..51f4de5c 100644 --- a/api/app/services/multimodal_service.py +++ b/api/app/services/multimodal_service.py @@ -14,9 +14,13 @@ import uuid from abc import ABC, abstractmethod from typing import List, Dict, Any, Optional +import csv +import json + import PyPDF2 import httpx import magic +import openpyxl from docx import Document from sqlalchemy.orm import Session @@ -39,6 +43,13 @@ DOC_MIME = [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ] +XLSX_MIME = [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'application/zip' +] +CSV_MIME = ['text/csv', 'application/csv'] +JSON_MIME = ['application/json'] class MultimodalFormatStrategy(ABC): @@ -577,6 +588,12 @@ class MultimodalService: return await self._extract_pdf_text(file_content) elif file_mime_type in DOC_MIME: return await self._extract_word_text(file_content) + elif file_mime_type in XLSX_MIME: + return await self._extract_xlsx_text(file_content) + elif file_mime_type in CSV_MIME: + return await self._extract_csv_text(file_content) + elif file_mime_type in JSON_MIME: + return await self._extract_json_text(file_content) else: return f"[Unsupported file type: {file_mime_type}]" except Exception as e: @@ -602,7 +619,6 @@ class MultimodalService: async def _extract_word_text(file_content: bytes) -> str: """提取 Word 文档文本""" try: - # 使用 BytesIO 读取 Word 文档 word_file = io.BytesIO(file_content) doc = Document(word_file) text_parts = [paragraph.text for paragraph in doc.paragraphs] @@ -611,6 +627,42 @@ class MultimodalService: logger.error(f"提取 Word 文本失败: {e}") return f"[Word 提取失败: {str(e)}]" + @staticmethod + async def _extract_xlsx_text(file_content: bytes) -> str: + """提取 Excel 文本""" + try: + wb = openpyxl.load_workbook(io.BytesIO(file_content), read_only=True, data_only=True) + parts = [] + for sheet in wb.worksheets: + parts.append(f"[Sheet: {sheet.title}]") + for row in sheet.iter_rows(values_only=True): + parts.append('\t'.join('' if v is None else str(v) for v in row)) + return '\n'.join(parts) + except Exception as e: + logger.error(f"提取 Excel 文本失败: {e}") + return f"[Excel 提取失败: {str(e)}]" + + @staticmethod + async def _extract_csv_text(file_content: bytes) -> str: + """提取 CSV 文本""" + try: + text = file_content.decode('utf-8-sig') + reader = csv.reader(io.StringIO(text)) + return '\n'.join('\t'.join(row) for row in reader) + except Exception as e: + logger.error(f"提取 CSV 文本失败: {e}") + return f"[CSV 提取失败: {str(e)}]" + + @staticmethod + async def _extract_json_text(file_content: bytes) -> str: + """提取 JSON 文本""" + try: + data = json.loads(file_content.decode('utf-8')) + return json.dumps(data, ensure_ascii=False, indent=2) + except Exception as e: + logger.error(f"提取 JSON 文本失败: {e}") + return f"[JSON 提取失败: {str(e)}]" + def get_multimodal_service(db: Session) -> MultimodalService: """获取多模态服务实例(依赖注入)""" From 83894df2605a5c1680bc6b2ff8446a70c2c29057 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 18 Mar 2026 10:52:07 +0800 Subject: [PATCH 10/24] fix(web): app sharing bugfix --- .../views/ApplicationConfig/components/AppSharingModal.tsx | 3 ++- web/src/views/ApplicationManagement/MySharing.tsx | 4 ++-- web/src/views/ApplicationManagement/index.tsx | 4 ++-- web/src/views/ApplicationManagement/types.ts | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/AppSharingModal.tsx b/web/src/views/ApplicationConfig/components/AppSharingModal.tsx index 39b2a77e..b49b10e3 100644 --- a/web/src/views/ApplicationConfig/components/AppSharingModal.tsx +++ b/web/src/views/ApplicationConfig/components/AppSharingModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-13 17:19:13 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-13 17:26:57 + * @Last Modified time: 2026-03-18 10:47:17 */ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Checkbox, App, Form } from 'antd'; @@ -147,6 +147,7 @@ const AppSharingModal = forwardRef(({ e.stopPropagation()} onChange={() => handleToggle(space.id, isShared)} /> {space.name} diff --git a/web/src/views/ApplicationManagement/MySharing.tsx b/web/src/views/ApplicationManagement/MySharing.tsx index 54e501ca..cc025a01 100644 --- a/web/src/views/ApplicationManagement/MySharing.tsx +++ b/web/src/views/ApplicationManagement/MySharing.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:34:12 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-13 17:36:16 + * @Last Modified time: 2026-03-18 10:44:32 */ import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -107,7 +107,7 @@ const MySharing: React.FC = () => { {items.map(item => (
handleCancelOne(item)} /> diff --git a/web/src/views/ApplicationManagement/index.tsx b/web/src/views/ApplicationManagement/index.tsx index 32652bc3..c9f57268 100644 --- a/web/src/views/ApplicationManagement/index.tsx +++ b/web/src/views/ApplicationManagement/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:34:12 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-16 09:56:02 + * @Last Modified time: 2026-03-18 10:50:33 */ /** * Application Management Page @@ -185,7 +185,7 @@ const ApplicationManagement: React.FC = () => { ref={scrollListRef} url={getApplicationListUrl} - query={{ ...query, shared_only: activeTab === 'sharing' }} + query={{ ...query, shared_only: activeTab === 'sharing', include_shared: activeTab !== 'apps' }} renderItem={(item) => ( Date: Wed, 18 Mar 2026 11:50:17 +0800 Subject: [PATCH 11/24] fix(app): The bugs that were fixed in the previous version but were later rolled back. --- api/app/services/app_service.py | 3 ++- api/app/services/multimodal_service.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index 1b0613e8..5ef34da8 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -2062,7 +2062,8 @@ class AppService: ) if memory_config_id: - updated_count = self._update_endusers_memory_config(app_id, memory_config_id) + + updated_count = self._update_endusers_memory_config_by_workspace(app.workspace_id, memory_config_id) logger.info( f"回滚时更新终端用户记忆配置: app_id={app_id}, version={version}, " f"memory_config_id={memory_config_id}, updated_count={updated_count}" diff --git a/api/app/services/multimodal_service.py b/api/app/services/multimodal_service.py index 51f4de5c..908ba953 100644 --- a/api/app/services/multimodal_service.py +++ b/api/app/services/multimodal_service.py @@ -588,7 +588,7 @@ class MultimodalService: return await self._extract_pdf_text(file_content) elif file_mime_type in DOC_MIME: return await self._extract_word_text(file_content) - elif file_mime_type in XLSX_MIME: + elif file_mime_type in XLSX_MIME and file.file_type.endswith(("xlsx", "xls")): return await self._extract_xlsx_text(file_content) elif file_mime_type in CSV_MIME: return await self._extract_csv_text(file_content) From 859b7f3c7f9cde9b09bbec713c51e402f3bd29da Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 18 Mar 2026 12:05:59 +0800 Subject: [PATCH 12/24] fix(web): my sharing app add empty --- .../views/ApplicationManagement/MySharing.tsx | 160 +++++++++--------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/web/src/views/ApplicationManagement/MySharing.tsx b/web/src/views/ApplicationManagement/MySharing.tsx index cc025a01..198c2c94 100644 --- a/web/src/views/ApplicationManagement/MySharing.tsx +++ b/web/src/views/ApplicationManagement/MySharing.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:34:12 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-18 10:44:32 + * @Last Modified time: 2026-03-18 11:20:45 */ import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ import clsx from 'clsx'; import type { MySharedOutItem } from './types'; import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application' +import BodyWrapper from '@/components/Empty/BodyWrapper' const MySharing: React.FC = () => { const { t } = useTranslation(); @@ -20,7 +21,8 @@ const MySharing: React.FC = () => { useEffect(() => { getList() }, []) const getList = () => { - mySharedOutList().then(res => setData(res as MySharedOutItem[])) + mySharedOutList() + .then(res => setData(res as MySharedOutItem[])) } /** Group items by target_workspace_id */ @@ -73,85 +75,87 @@ const MySharing: React.FC = () => { }; return ( - - {grouped.map(({ workspace, items }) => ( - - {workspace.target_workspace_icon - ? - :
- {workspace.target_workspace_name[0]} -
- } -
- {workspace.target_workspace_name} -
{t('application.appCount', { count: items.length })}
-
-
- ), - extra: ( - - ), - children: ( - - {items.map(item => ( - -
handleCancelOne(item)} - /> - -
- {item.source_app_name[0]} + + + {grouped.map(({ workspace, items }) => ( + + {workspace.target_workspace_icon + ? + :
+ {workspace.target_workspace_name[0]}
-
{item.source_app_name}
-
- - - {t('application.type')} - - {t(`application.${item.source_app_type}`)} - + } +
+ {workspace.target_workspace_name} +
{t('application.appCount', { count: items.length })}
+
+
+ ), + extra: ( + + ), + children: ( + + {items.map(item => ( + +
handleCancelOne(item)} + /> + +
+ {item.source_app_name[0]} +
+
{item.source_app_name}
- - {t('application.version')} - {item.source_app_version} + + + {t('application.type')} + + {t(`application.${item.source_app_type}`)} + + + + {t('application.version')} + {item.source_app_version} + + + {t('application.permission')} + + {t(`application.${item.permission}`)} + + + + {t('application.souceStatus')} + {item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')} + - - {t('application.permission')} - - {t(`application.${item.permission}`)} - - - - {t('application.souceStatus')} - {item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')} - - - - ))} - - ), - }]} - /> - ))} - + + ))} + + ), + }]} + /> + ))} + + ); }; From 65dc1a8f489b737633bd082af4e1c21a2e1bd826 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 18 Mar 2026 12:07:29 +0800 Subject: [PATCH 13/24] fix(web): workflow node ports bugfix --- .../Workflow/components/PortClickHandler.tsx | 35 ++++++++++++------- .../views/Workflow/hooks/useWorkflowGraph.ts | 35 +++++++++++++------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index ec898bc8..903ccbdc 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-09 18:30:28 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 18:30:28 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-18 12:06:27 */ import { useEffect, useState } from 'react'; import { Popover } from 'antd'; @@ -70,7 +70,6 @@ const PortClickHandler: React.FC = ({ graph }) => { // Get source port group information const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort); const sourcePortGroup = sourcePortInfo?.group || sourcePort; - console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo) // If add-node position exists, use it; otherwise calculate new position let newX, newY; @@ -148,18 +147,23 @@ const PortClickHandler: React.FC = ({ graph }) => { if (sourcePortGroup === 'left') { // Connect from left port to new node's right side targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right'; + graph.addEdge({ + source: { cell: newNode.id, port: targetPort }, + target: { cell: sourceNode.id, port: sourcePort }, + ...edgeAttrs + // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 + }); } else { // Connect from right port to new node's left side targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left'; + graph.addEdge({ + source: { cell: sourceNode.id, port: sourcePort }, + target: { cell: newNode.id, port: targetPort }, + ...edgeAttrs + // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 + }); } - graph.addEdge({ - source: { cell: sourceNode.id, port: sourcePort }, - target: { cell: newNode.id, port: targetPort }, - ...edgeAttrs - // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0 - }); - // Adjust loop node size when child node is added via port within loop node const cycleId = sourceNodeData.cycle; if (cycleId) { @@ -223,20 +227,27 @@ const PortClickHandler: React.FC = ({ graph }) => { const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration'); + const sourcePortInfo = sourceNode?.getPorts().find((p: any) => p.id === sourcePort); + const sourcePortGroup = sourcePortInfo?.group || sourcePort; + const isLeftPort = sourcePortGroup === 'left'; + let filteredNodes; if (isChildOfLoop) { - // Use same filtering as AddNode for child nodes of loop, but allow break + // Use same filtering as AddNode for child nodes of loop, but allow break filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else if (isChildOfIteration) { // Filter out loop and iteration nodes for children of iteration nodes, but allow break filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else { // Original filtering for non-loop child nodes - filteredNodes = category.nodes.filter(nodeType => !['start', 'break', 'cycle-start'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => nodeType.type !== 'start' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' ); } + + if (isLeftPort) { + filteredNodes = filteredNodes.filter(nodeType => nodeType.type !== 'end'); + } if (filteredNodes.length === 0) return null; diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 050c1680..db0c3c93 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:17:48 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 10:00:10 + * @Last Modified time: 2026-03-18 12:07:03 */ import { useRef, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -593,13 +593,6 @@ export const useWorkflowGraph = ({ if (!graphRef.current) return false; const selectedNodes = graphRef.current.getNodes().filter(node => node.getData()?.isSelected); if (selectedNodes.length) { - selectedNodes.forEach(node => { - const data = node.getData(); - node.setData({ - ...data, - id: `${(data.type as string).replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - }); - }); graphRef.current.copy(selectedNodes); } return false; @@ -610,7 +603,14 @@ export const useWorkflowGraph = ({ */ const parseEvent = () => { if (!graphRef.current?.isClipboardEmpty()) { - graphRef.current?.paste({ offset: 32 }); + const pastedNodes = graphRef.current?.paste({ offset: 32 }) ?? []; + pastedNodes.forEach(cell => { + if (cell.isNode()) { + const data = cell.getData(); + const newId = `${(data.type as string).replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + cell.setData({ ...data, id: newId }); + } + }); blankClick(); } return false; @@ -761,8 +761,23 @@ export const useWorkflowGraph = ({ createEdge() { return graphRef.current?.createEdge(edgeAttrs); }, - validateConnection({ sourceCell, targetCell, targetMagnet }) { + validateConnection({ sourceCell, targetCell, sourceMagnet, targetMagnet }) { if (!targetMagnet) return false; + + // Only allow right port → left port connections + const getPortGroup = (magnet: Element) => { + let el: Element | null = magnet; + while (el) { + const group = el.getAttribute('port-group'); + if (group) return group; + el = el.parentElement; + } + return null; + }; + const sourceGroup = sourceMagnet ? getPortGroup(sourceMagnet) : null; + const targetGroup = targetMagnet ? getPortGroup(targetMagnet) : null; + + if (sourceGroup === 'left' || targetGroup === 'right') return false; // Node cannot connect to itself if (sourceCell?.id === targetCell?.id) return false; From 969d42832000f5011caf6bed9825b771ec957fa0 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 18 Mar 2026 14:03:06 +0800 Subject: [PATCH 14/24] fix(web): agent add tools bugfix --- .../ApplicationConfig/components/ToolList/ToolList.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx index c93bc3e3..5ce84554 100644 --- a/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx +++ b/web/src/views/ApplicationConfig/components/ToolList/ToolList.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:26:03 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 15:53:06 + * @Last Modified time: 2026-03-18 14:01:13 */ /** * Tool List Component @@ -107,7 +107,10 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) => } /** Add new tool to list */ const updateTools = (tool: ToolOption) => { - const list = [...toolList, tool] + const list = [...toolList, { + ...tool, + is_active: true, + }] setToolList(list) onChange && onChange(list) } From 4bb2ccfba7a2d7997ae6222bb564308e22132bc9 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 18 Mar 2026 14:36:23 +0800 Subject: [PATCH 15/24] fix(web): app bugfix --- web/src/utils/stream.ts | 20 ++++++++++++------- .../views/ApplicationConfig/ReleasePage.tsx | 5 +++-- .../ApplicationConfig/TestChat/index.tsx | 8 ++++++-- .../views/Workflow/components/Chat/Chat.tsx | 6 +++--- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/web/src/utils/stream.ts b/web/src/utils/stream.ts index 846af9f7..ba966159 100644 --- a/web/src/utils/stream.ts +++ b/web/src/utils/stream.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 16:35:43 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-04 18:19:24 + * @Last Modified time: 2026-03-18 14:32:40 */ /** * Server-Sent Events (SSE) Stream Utility Module @@ -176,17 +176,23 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe case 500: case 502: const errorData = await response.json(); - let errorInfo = errorData.error || i18n.t('common.serviceUpgrading') + const errorInfo = errorData.error || i18n.t('common.serviceUpgrading'); message.warning(errorInfo); - throw errorInfo; + throw new Error(errorData); case 400: const error = await response.json(); - message.warning(error.error); - throw error.error || 'Bad Request'; + const error400 = error.error || 'Bad Request'; + message.warning(error400); + throw new Error(error); + case 403: + const errors = await response.json(); + message.warning(i18n.t('common.permissionDenied')); + throw new Error(errors); case 504: const errorJson = await response.json(); - message.warning(errorJson.error || i18n.t('common.serverError')); - throw errorData.error; + const errorMsg = errorJson.error || i18n.t('common.serverError'); + message.warning(errorMsg); + throw new Error(errorJson); case 401: if (url?.includes('/public')) { return message.warning(i18n.t('common.publicApiCannotRefreshToken')); diff --git a/web/src/views/ApplicationConfig/ReleasePage.tsx b/web/src/views/ApplicationConfig/ReleasePage.tsx index ab9225f6..efa62578 100644 --- a/web/src/views/ApplicationConfig/ReleasePage.tsx +++ b/web/src/views/ApplicationConfig/ReleasePage.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:41 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-11 17:44:24 + * @Last Modified time: 2026-03-18 14:30:41 */ import { type FC, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -70,7 +70,8 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres }) } const handleExport = () => { - appExport(data.id, data.name) + if (!selectedVersion) return + appExport(data.id, data.name, {release_version: selectedVersion.id}) } return (
diff --git a/web/src/views/ApplicationConfig/TestChat/index.tsx b/web/src/views/ApplicationConfig/TestChat/index.tsx index bb37c5a8..b7ce167e 100644 --- a/web/src/views/ApplicationConfig/TestChat/index.tsx +++ b/web/src/views/ApplicationConfig/TestChat/index.tsx @@ -193,7 +193,10 @@ const TestChat: FC = ({ formatParams(message, conversationId, files, params), handleStreamMessage ) - .catch(() => setLoading(false)) + .catch(() => { + updateErrorAssistantMessage(0) + setLoading(false) + }) .finally(() => { setLoading(false) setStreamLoading(false) @@ -243,11 +246,12 @@ const TestChat: FC = ({ handleWorkflowStreamMessage ) .catch((error) => { + const errorInfo = JSON.parse(error.message) setChatList(prev => { const newList = [...prev] const lastIndex = newList.length - 1 if (lastIndex >= 0) { - newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: error.error } + newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: errorInfo.error } } return newList }) diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 8e744d6a..37cb215e 100644 --- a/web/src/views/Workflow/components/Chat/Chat.tsx +++ b/web/src/views/Workflow/components/Chat/Chat.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:10:56 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 15:05:21 + * @Last Modified time: 2026-03-18 14:34:20 */ /** * Workflow Chat Component @@ -359,7 +359,7 @@ const Chat = forwardRef { - console.log('draftRun error', error) + const errorInfo = JSON.parse(error.message) setChatList(prev => { const newList = [...prev] const lastIndex = newList.length - 1 @@ -368,7 +368,7 @@ const Chat = forwardRef Date: Wed, 18 Mar 2026 16:10:20 +0800 Subject: [PATCH 16/24] fix(web): app features --- web/src/components/Chat/ChatToolbar.tsx | 12 +++---- web/src/i18n/zh.ts | 2 +- web/src/views/ApplicationConfig/Agent.tsx | 3 +- web/src/views/ApplicationConfig/Cluster.tsx | 3 +- .../components/AppSharingModal.tsx | 17 +++++---- .../components/ConfigHeader.tsx | 18 +++++----- .../FeaturesConfig/FeaturesConfigModal.tsx | 35 +++++++++++-------- .../components/FeaturesConfig/index.tsx | 9 +++-- web/src/views/ApplicationConfig/index.tsx | 9 +++-- .../views/ApplicationManagement/MySharing.tsx | 9 +++-- .../components/UploadFileListModal.tsx | 3 +- web/src/views/Conversation/index.tsx | 24 +++++++------ .../Workflow/components/CanvasToolbar.tsx | 4 --- .../views/Workflow/hooks/useWorkflowGraph.ts | 21 ++++++++--- web/src/views/Workflow/index.tsx | 20 ++++------- 15 files changed, 111 insertions(+), 78 deletions(-) diff --git a/web/src/components/Chat/ChatToolbar.tsx b/web/src/components/Chat/ChatToolbar.tsx index 6a316bd5..883ac98a 100644 --- a/web/src/components/Chat/ChatToolbar.tsx +++ b/web/src/components/Chat/ChatToolbar.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-17 14:22:25 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-17 18:39:49 + * @Last Modified time: 2026-03-18 15:55:13 */ // Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react' @@ -120,7 +120,10 @@ const ChatToolbar = forwardRef(({ // Build dropdown menu items based on allowed transfer methods const fileMenus: MenuProps['items'] = [] - if (file_upload?.allowed_transfer_methods?.includes('remote_url')) { + const enabledTypes = ['image', 'document', 'video', 'audio'].filter( + type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']] + ) + if (file_upload?.allowed_transfer_methods?.includes('remote_url') && enabledTypes.length > 0) { fileMenus.push({ key: 'url', label: t('memoryConversation.addRemoteFile'), @@ -133,9 +136,6 @@ const ChatToolbar = forwardRef(({ } }) } - const enabledTypes = ['image', 'document', 'video', 'audio'].filter( - type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']] - ) if (file_upload?.allowed_transfer_methods?.includes('local_file') && enabledTypes.length > 0) { fileMenus.push({ key: 'upload', @@ -155,7 +155,7 @@ const ChatToolbar = forwardRef(({ -