From b272a52b578e75fbff158f2a8d1adc1bf0a11e93 Mon Sep 17 00:00:00 2001 From: Ke Sun <33739460+keeees@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:19:32 +0800 Subject: [PATCH] Release/v0.2.4 (#397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix/bug en zh (#389) * [fix]The log retains genuine alerts and errors, while filtering out unnecessary noise. * [fix]Scenario English and Chinese, emotion specifications * [fix]Change the "no data" scenario from 0.0 to None * [fix]The emotional health indicators, emotional advice, and emotional distribution analysis are all linked together. * [fix]The emotional health indicators, emotional advice, and emotional distribution analysis are all linked together. * [fix]Separate expected errors from unexpected errors * [changes]Translation of emotion labels, and the list of hosts arranged in the order of creation * [changes]Translation of emotion labels, and the list of hosts arranged in the order of creation * feat(web): improve knowledge base form validation and parser config handling - Refactor form validation logic to support tab-specific field validation in edit mode - Add conditional validation for knowledge graph fields when editing existing knowledge base - Preserve all existing parser_config fields when merging graphrag configuration - Skip third-party authentication check when editing on knowledge graph tab - Update form value retrieval to include disabled fields using getFieldsValue(true) - Improve comments to clarify parser_config field preservation and validation behavior - This change enables users to edit knowledge graph settings without re-validating all basic configuration fields * fix(web): improve infinite scroll handling in knowledge base list - Add auto-load detection when initial data doesn't fill viewport to prevent empty scrollbar - Implement scroll height check to automatically load more data if content is insufficient - Fix hasMore condition to prevent premature loader hiding - Update loader visibility to only show when data exists and is actively loading - Refine end message display to show only when all data is loaded and no more items available - Resolves issue where knowledge base list shows no scrollbar on initial load with limited items * fix(web): FileUpload bugfix * fix(web): change skill search key * Fix/bug en zh (#391) * [fix]The log retains genuine alerts and errors, while filtering out unnecessary noise. * [fix]Scenario English and Chinese, emotion specifications * [fix]Change the "no data" scenario from 0.0 to None * [fix]The emotional health indicators, emotional advice, and emotional distribution analysis are all linked together. * [fix]The emotional health indicators, emotional advice, and emotional distribution analysis are all linked together. * [fix]Separate expected errors from unexpected errors * [changes]Translation of emotion labels, and the list of hosts arranged in the order of creation * [changes]Translation of emotion labels, and the list of hosts arranged in the order of creation * [fix]The mainframe engineering supports Chinese verification. * [fix]The mainframe engineering supports Chinese verification. * fix(web): update en * fix(web): file upload bugfix * fix(web): memory-write node hide message config --------- Co-authored-by: 乐力齐 <162269739+lanceyq@users.noreply.github.com> Co-authored-by: yujiangping Co-authored-by: zhaoying Co-authored-by: yingzhao --- .../memory/models/ontology_scenario_models.py | 13 +++-- .../utils/validation/ontology_validator.py | 12 +++-- api/app/services/emotion_analytics_service.py | 6 +-- api/app/services/memory_dashboard_service.py | 6 +-- web/src/i18n/en.ts | 1 + web/src/views/ApplicationConfig/Agent.tsx | 10 ++-- .../ApplicationConfig/components/Chat.tsx | 48 +++++++++---------- .../components/Skill/SkillListModal.tsx | 12 ++--- .../Conversation/components/FileUpload.tsx | 17 ++----- web/src/views/Conversation/index.tsx | 6 +-- .../KnowledgeBase/components/CreateModal.tsx | 34 ++++++++++--- web/src/views/KnowledgeBase/index.tsx | 18 +++++-- .../views/Workflow/components/Chat/Chat.tsx | 6 +-- .../Workflow/components/Properties/index.tsx | 3 +- web/src/views/Workflow/constant.ts | 5 +- 15 files changed, 109 insertions(+), 88 deletions(-) diff --git a/api/app/core/memory/models/ontology_scenario_models.py b/api/app/core/memory/models/ontology_scenario_models.py index 24a61f5f..b51d8bb2 100644 --- a/api/app/core/memory/models/ontology_scenario_models.py +++ b/api/app/core/memory/models/ontology_scenario_models.py @@ -74,7 +74,7 @@ class OntologyClass(BaseModel): """Validate that the class name follows PascalCase convention. PascalCase rules: - - Must start with an uppercase letter + - Must start with an uppercase letter (for English) or any character (for Chinese/Unicode) - Cannot contain spaces - Should not contain special characters except underscores @@ -90,7 +90,10 @@ class OntologyClass(BaseModel): if not v: raise ValueError("Class name cannot be empty") - if not v[0].isupper(): + # For Chinese/Unicode characters, skip the uppercase check + # Only check uppercase for ASCII letters + first_char = v[0] + if first_char.isascii() and first_char.isalpha() and not first_char.isupper(): raise ValueError( f"Class name '{v}' must start with an uppercase letter (PascalCase)" ) @@ -100,11 +103,11 @@ class OntologyClass(BaseModel): f"Class name '{v}' cannot contain spaces (PascalCase)" ) - # Check for invalid characters (allow alphanumeric and underscore only) - if not all(c.isalnum() or c == '_' for c in v): + # Check for invalid characters (allow alphanumeric, underscore, and Unicode characters) + if not all(c.isalnum() or c == '_' or ord(c) > 127 for c in v): raise ValueError( f"Class name '{v}' contains invalid characters. " - "Only alphanumeric characters and underscores are allowed" + "Only alphanumeric characters, underscores, and Unicode characters are allowed" ) return v diff --git a/api/app/core/memory/utils/validation/ontology_validator.py b/api/app/core/memory/utils/validation/ontology_validator.py index 1e1ee506..cb3bcec8 100644 --- a/api/app/core/memory/utils/validation/ontology_validator.py +++ b/api/app/core/memory/utils/validation/ontology_validator.py @@ -88,8 +88,10 @@ class OntologyValidator: logger.warning(f"Validation failed: {error_msg}") return False, error_msg - # Check if starts with uppercase letter - if not name[0].isupper(): + # Check if starts with uppercase letter (only for ASCII letters) + # For Chinese/Unicode characters, skip this check + first_char = name[0] + if first_char.isascii() and first_char.isalpha() and not first_char.isupper(): error_msg = f"Class name '{name}' must start with an uppercase letter (PascalCase)" logger.warning(f"Validation failed: {error_msg}") return False, error_msg @@ -100,9 +102,9 @@ class OntologyValidator: logger.warning(f"Validation failed: {error_msg}") return False, error_msg - # Check for invalid characters (only alphanumeric and underscore allowed) - if not re.match(r'^[A-Za-z0-9_]+$', name): - error_msg = f"Class name '{name}' contains invalid characters. Only alphanumeric characters and underscores are allowed" + # Check for invalid characters (allow alphanumeric, underscore, and Unicode characters) + if not re.match(r'^[A-Za-z0-9_\u4e00-\u9fff]+$', name): + error_msg = f"Class name '{name}' contains invalid characters. Only alphanumeric characters, underscores, and Chinese characters are allowed" logger.warning(f"Validation failed: {error_msg}") return False, error_msg diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index d78dc20c..89e3cab9 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -124,17 +124,17 @@ class EmotionAnalyticsService: # 将查询结果转换为字典,方便查找 tags_dict = {tag["emotion_type"]: tag for tag in tags} - # 补全缺失的情绪维度,并翻译 emotion_type + # 补全缺失的情绪维度,直接使用英文枚举key(前端自行翻译) complete_tags = [] for emotion in all_emotion_types: if emotion in tags_dict: tag = tags_dict[emotion].copy() - tag["emotion_type"] = self._translate_emotion_type(emotion, language) + tag["emotion_type"] = emotion complete_tags.append(tag) else: # 如果该情绪类型不存在,添加默认值 complete_tags.append({ - "emotion_type": self._translate_emotion_type(emotion, language), + "emotion_type": emotion, "count": 0, "percentage": 0.0, "avg_intensity": 0.0 diff --git a/api/app/services/memory_dashboard_service.py b/api/app/services/memory_dashboard_service.py index 6fa8b228..8d6071cc 100644 --- a/api/app/services/memory_dashboard_service.py +++ b/api/app/services/memory_dashboard_service.py @@ -55,7 +55,7 @@ def get_workspace_end_users( ) -> List[EndUser]: """获取工作空间的所有宿主(优化版本:减少数据库查询次数) - 返回结果按 updated_at 从新到旧排序(NULL 值排在最后) + 返回结果按 created_at 从新到旧排序(NULL 值排在最后) """ business_logger.info(f"获取工作空间宿主列表: workspace_id={workspace_id}, 操作者: {current_user.username}") @@ -71,13 +71,13 @@ def get_workspace_end_users( app_ids = [app.id for app in apps_orm] # 批量查询所有 end_users(一次查询而非循环查询) - # 按 updated_at 降序排序,NULL 值排在最后;id 作为次级排序键保证确定性 + # 按 created_at 降序排序,NULL 值排在最后;id 作为次级排序键保证确定性 from app.models.end_user_model import EndUser as EndUserModel from sqlalchemy import desc, nullslast end_users_orm = db.query(EndUserModel).filter( EndUserModel.app_id.in_(app_ids) ).order_by( - nullslast(desc(EndUserModel.updated_at)), + nullslast(desc(EndUserModel.created_at)), desc(EndUserModel.id) ).all() diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 9892b728..7f013752 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1289,6 +1289,7 @@ export const en = { priority: 'Structured Integration', addTool: 'Add Tool', tool: 'Tool', + variableConfig: 'Variable Configuration', statistics: 'Data Statistics', daily_conversations: 'Daily Conversations', diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index a0587798..4c3b73ba 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:21 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 16:56:27 + * @Last Modified time: 2026-02-10 18:46:40 */ import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'; import clsx from 'clsx' @@ -480,9 +480,11 @@ const Agent = forwardRef((_props, ref) => { {t('application.debuggingAndPreview')} - + {chatVariables.length > 0 && + + } diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index effb34c3..794489c6 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:39 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-10 12:18:23 + * @Last Modified time: 2026-02-10 17:40:15 */ /** * Chat debugging component for application testing @@ -366,10 +366,8 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc const handleMessageChange = (message: string) => { setMessage(message) } - const [update, setUpdate] = useState(false) const fileChange = (file?: any) => { setFileList([...fileList, file]) - setUpdate(prev => !prev) } // const handleRecordingComplete = async (file: any) => { // console.log('file', file) @@ -456,29 +454,27 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc onChange={handleMessageChange} > - - - ) - }, - ], - onClick: handleShowUpload - }} - > -
-
+ + + ) + }, + ], + onClick: handleShowUpload + }} + > +
+
{/* diff --git a/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx index 8220878e..0a56b82f 100644 --- a/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx +++ b/web/src/views/ApplicationConfig/components/Skill/SkillListModal.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-05 10:45:08 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-05 10:45:08 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 17:59:37 */ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { Space, List, Flex, Tooltip } from 'antd'; @@ -31,7 +31,7 @@ interface SkillModalProps { * * A modal dialog for selecting skills from a searchable list. * Features: - * - Search functionality to filter skills by keywords + * - Search functionality to filter skills by search * - Grid layout displaying skill cards with icons and descriptions * - Multi-select capability with visual feedback * - Excludes already selected skills from the list @@ -49,7 +49,7 @@ const SkillListModal = forwardRef(({ const [visible, setVisible] = useState(false); const [list, setList] = useState([]) const [filterList, setFilterList] = useState([]) - const [query, setQuery] = useState<{keywords?: string}>({}) + const [query, setQuery] = useState<{search?: string}>({}) const [selectedIds, setSelectedIds] = useState([]) const [selectedRows, setSelectedRows] = useState([]) @@ -82,7 +82,7 @@ const SkillListModal = forwardRef(({ if (visible) { getList() } - }, [query.keywords, visible]) + }, [query.search, visible]) /** * Fetches the skill list from API with current search parameters @@ -123,7 +123,7 @@ const SkillListModal = forwardRef(({ * @param value - Search keyword */ const handleSearch = (value?: string) => { - setQuery({keywords: value}) + setQuery({search: value}) setSelectedIds([]) setSelectedRows([]) } diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index b7d65f80..70ee9cf2 100644 --- a/web/src/views/Conversation/components/FileUpload.tsx +++ b/web/src/views/Conversation/components/FileUpload.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 16:41:31 + * @Last Modified time: 2026-02-11 11:32:48 */ /** * File Upload Component @@ -55,8 +55,6 @@ interface UploadFilesProps extends Omit { maxCount?: number; /** Custom file removal callback */ onRemove?: (file: UploadFile) => boolean | void | Promise; - /** Trigger to reset file list */ - update?: boolean; } // Mapping of file extensions to MIME types const ALL_FILE_TYPE: { @@ -109,7 +107,6 @@ const UploadFiles = forwardRef(({ isAutoUpload = true, maxCount = 1, onRemove: customOnRemove, - update, requestConfig, ...props }, ref) => { @@ -118,11 +115,6 @@ const UploadFiles = forwardRef(({ const [fileList, setFileList] = useState(propFileList); const [accept, setAccept] = useState(); - // Reset file list when update prop changes - useEffect(() => { - setFileList([]) - }, [update]) - /** * Validates file type and size before upload * @returns Upload.LIST_IGNORE to prevent upload, or true to proceed @@ -175,7 +167,7 @@ const UploadFiles = forwardRef(({ formData.append('file', file); const response = await request.uploadFile(action, formData, requestConfig); - + onSuccess?.({data: response}); } catch (error) { onError?.(error as Error); @@ -185,11 +177,10 @@ const UploadFiles = forwardRef(({ /** * Handles upload state changes */ - const handleChange: UploadProps['onChange'] = ({ fileList: newFileList, event }) => { - console.log('event', event) + const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { setFileList(newFileList); if (onChange) { - onChange(maxCount === 1 ? newFileList[0] : newFileList); + onChange(maxCount === 1 ? newFileList[newFileList.length - 1] : newFileList); } }; diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index fcc32bf8..825ea834 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-02-09 20:20:01 + * @Last Modified time: 2026-02-10 17:41:05 */ /** * Conversation Page @@ -254,10 +254,8 @@ const Conversation: FC = () => { }) } - const [update, setUpdate] = useState(false) const fileChange = (file?: any) => { form.setFieldValue('files', [...(queryValues.files || []), file]) - setUpdate(prev => !prev) } // const handleRecordingComplete = async (file: any) => { // console.log('file', file) @@ -353,8 +351,6 @@ const Conversation: FC = () => { action={shareFileUploadUrlWithoutApiPrefix} fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']} onChange={fileChange} - fileList={[]} - update={update} requestConfig={{ headers: { 'Content-Type': 'multipart/form-data', diff --git a/web/src/views/KnowledgeBase/components/CreateModal.tsx b/web/src/views/KnowledgeBase/components/CreateModal.tsx index 71fe1b94..76640058 100644 --- a/web/src/views/KnowledgeBase/components/CreateModal.tsx +++ b/web/src/views/KnowledgeBase/components/CreateModal.tsx @@ -221,9 +221,9 @@ const CreateModal = forwardRef(({ status: record.status, }; - // Process parser_config data, set default values if not present + // Process parser_config data, preserve all existing fields and merge graphrag config baseValues.parser_config = { - ...record.parser_config, + ...record.parser_config, // Preserve all existing parser_config fields (yuque_user_id, yuque_token, feishu_app_id, etc.) graphrag: { use_graphrag: false, scene_name: '', @@ -362,12 +362,32 @@ const CreateModal = forwardRef(({ // Actual save logic const performSave = async () => { try { - await form.validateFields(); - setLoading(true); - const formValues = form.getFieldsValue(); + // Get fields to validate based on current tab and edit mode + let fieldsToValidate: any[] | undefined = undefined; - // Check Third-party authentication before saving - if (formValues.type === 'Third-party' || currentType === 'Third-party') { + // If in edit mode and on knowledge graph tab, only validate knowledge graph fields + if (datasets?.id && activeTab === 'knowledgeGraph') { + fieldsToValidate = [ + ['parser_config', 'graphrag', 'use_graphrag'], + ['parser_config', 'graphrag', 'scene_name'], + ['parser_config', 'graphrag', 'entity_types'], + ['parser_config', 'graphrag', 'method'], + ['parser_config', 'graphrag', 'resolution'], + ['parser_config', 'graphrag', 'community'], + ]; + } + + // Validate only specified fields or all fields + await form.validateFields(fieldsToValidate); + setLoading(true); + // Get all field values including disabled fields + const formValues = form.getFieldsValue(true); + + // Only check Third-party authentication when creating new or explicitly on basic config tab + // Skip authentication check when editing and on knowledge graph tab + const shouldCheckAuth = !datasets?.id || activeTab === 'basic'; + + if (shouldCheckAuth && (formValues.type === 'Third-party' || currentType === 'Third-party')) { const platform = formValues.parser_config?._third_party_platform || thirdPartyPlatform; try { diff --git a/web/src/views/KnowledgeBase/index.tsx b/web/src/views/KnowledgeBase/index.tsx index cef520ab..1ad6997b 100644 --- a/web/src/views/KnowledgeBase/index.tsx +++ b/web/src/views/KnowledgeBase/index.tsx @@ -334,6 +334,18 @@ const KnowledgeBaseManagement: FC = () => { setHasMore(hasNext); buildModelMenus(list, isLoadMore); + + // 首次加载后,检查是否需要自动加载更多(解决无滚动条问题) + if (!isLoadMore && hasNext) { + setTimeout(() => { + const scrollDiv = document.getElementById('scrollableDiv'); + if (scrollDiv && scrollDiv.scrollHeight <= scrollDiv.clientHeight) { + console.log('No scrollbar detected, auto-loading more data'); + setPage(2); + fetchData(2, true); + } + }, 100); + } } catch (error) { console.error('Failed to fetch knowledge base list:', error); if (!isLoadMore) { @@ -532,10 +544,10 @@ const KnowledgeBaseManagement: FC = () => { {t('common.loading')}} + hasMore={hasMore} + loader={loading && data.length > 0 ?
{t('common.loading')}
: null} endMessage={ - data.length > 0 ? ( + data.length > 0 && !hasMore ? (
{t('common.noMoreData')}
diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 95f43a9c..00a0c7fb 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-02-10 12:17:41 + * @Last Modified time: 2026-02-10 17:41:24 */ /** * Workflow Chat Component @@ -343,13 +343,11 @@ const Chat = forwardRef(({ appId const handleMessageChange = (message: string) => { setMessage(message) } - const [update, setUpdate] = useState(false) /** * Handles file upload from local device */ const fileChange = (file?: any) => { setFileList([...fileList, file]) - setUpdate(prev => !prev) } // const handleRecordingComplete = async (file: any) => { // console.log('file', file) @@ -517,8 +515,6 @@ const Chat = forwardRef(({ appId ) }, diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 9e6e418b..e96b1757 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:39:59 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 19:56:42 + * @Last Modified time: 2026-02-11 12:07:06 */ import { type FC, useEffect, useState, useMemo } from "react"; import clsx from 'clsx' @@ -627,6 +627,7 @@ const Properties: FC = ({ } layout={config.type === 'switch' ? 'horizontal' : 'vertical'} className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''} + hidden={Boolean(config.hidden)} > {config.type === 'input' ? diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 6b36dec5..5ae3e5b0 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:06:18 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 20:08:03 + * @Last Modified time: 2026-02-11 12:07:20 */ import LoopNode from './components/Nodes/LoopNode'; import NormalNode from './components/Nodes/NormalNode'; @@ -240,7 +240,8 @@ export const nodeLibrary: NodeLibrary[] = [ config: { message: { type: 'editor', - isArray: false + isArray: false, + hidden: true, }, messages: { type: 'messageEditor',