From 33238d34c94d421050c5c0579c6bb1e005bd05a8 Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Thu, 26 Feb 2026 10:17:44 +0800 Subject: [PATCH 01/83] [fix]Force re-importing Trio in child processes (to avoid inheriting the state of the parent process) --- api/app/tasks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/app/tasks.py b/api/app/tasks.py index d60af6e5..499b93a5 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -255,7 +255,7 @@ def parse_document(file_path: str, document_id: uuid.UUID): progress_msg += f"{datetime.now().strftime('%H:%M:%S')} GraphRAG task result for task {task}:\n{result}\n" return result - try: + def sync_task(): trio.run( lambda: _run( row=task, @@ -270,6 +270,10 @@ def parse_document(file_path: str, document_id: uuid.UUID): with_community=with_community, ) ) + try: + with ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(sync_task) + future.result() # Blocks until the task completes except Exception as e: progress_msg += f"{datetime.now().strftime('%H:%M:%S')} GraphRAG task failed for task {task}:\n{str(e)}\n" progress_msg += f"{datetime.now().strftime('%H:%M:%S')} Knowledge Graph done ({time.time() - start_time}s)" From 4f0b653a822119c0ccf2fbbc0b75262113743794 Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Thu, 26 Feb 2026 19:04:42 +0800 Subject: [PATCH 02/83] =?UTF-8?q?=E3=80=90fix]The=20complexity=20and=20vol?= =?UTF-8?q?ume=20of=20the=20document=20content=20require=20an=20extended?= =?UTF-8?q?=20timeframe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/celery_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/app/celery_app.py b/api/app/celery_app.py index db78a368..265cd2ab 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -43,8 +43,8 @@ celery_app.conf.update( task_ignore_result=False, # 超时设置 - task_time_limit=1800, # 30分钟硬超时 - task_soft_time_limit=1500, # 25分钟软超时 + task_time_limit=3600, # 60分钟硬超时 + task_soft_time_limit=3000, # 50分钟软超时 # Worker 设置 (per-worker settings are in docker-compose command line) worker_prefetch_multiplier=1, # Don't hoard tasks, fairer distribution From 5c42a84c3e343b70b74fda5a244498e37f7285db Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 3 Mar 2026 15:09:16 +0800 Subject: [PATCH 03/83] fix(web): Implicit detail add check data api --- web/src/api/memory.ts | 9 +++++++-- web/src/i18n/en.ts | 3 ++- web/src/i18n/zh.ts | 3 ++- .../UserMemoryDetail/pages/ImplicitDetail.tsx | 16 ++++++++++++++-- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 987ef358..cb917ec1 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 14:00:06 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 14:00:06 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-03 14:58:32 */ import { request } from '@/utils/request' import type { @@ -163,9 +163,14 @@ export const getImplicitInterestAreas = (end_user_id: string) => { export const getImplicitHabits = (end_user_id: string) => { return request.get(`/memory/implicit-memory/habits/${end_user_id}`) } +// Implicit Memory - Generate user portrait export const generateProfile = (end_user_id: string) => { return request.post(`/memory/implicit-memory/generate_profile`, { end_user_id }) } +// Implicit Memory - Check if data exists +export const implicitCheckData = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/check-data/${end_user_id}`) +} // Short-term memory export const getShortTerm = (end_user_id: string) => { return request.get(`/memory/short/short_term`, { end_user_id }) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index f2b4eaa4..b17ad291 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2522,7 +2522,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re context_details: 'Preference Details', supporting_evidence: 'Preference Source', specific_examples: 'Source', - wordEmpty: 'Click on a node in the left chart to view preference details' + wordEmpty: 'Click on a node in the left chart to view preference details', + noData: 'Portrait data does not exist, please click the refresh button in the top right corner to initialize', }, shortTermDetail: { title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index e2e7082a..181173ff 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2518,7 +2518,8 @@ export const zh = { context_details: '偏好详情', supporting_evidence: '偏好来源', specific_examples: '来源', - wordEmpty: '点击左侧图表中的节点查看偏好详情' + wordEmpty: '点击左侧图表中的节点查看偏好详情', + noData: '画像数据不存在,请点击右上角刷新进行初始化', }, shortTermDetail: { title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。', diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx index dfe5c1ee..351e5ed1 100644 --- a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -1,6 +1,6 @@ -import { forwardRef, useImperativeHandle, useRef } from 'react' +import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Row, Col } from 'antd' +import { Row, Col, App } from 'antd' import { useParams } from 'react-router-dom' import Preferences from '../components/Preferences' @@ -9,15 +9,27 @@ import InterestAreas from '../components/InterestAreas' import Habits from '../components/Habits' import { generateProfile, + implicitCheckData, } from '@/api/memory' const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => { const { t } = useTranslation() const { id } = useParams() + const { message } = App.useApp() const preferencesRef = useRef<{ handleRefresh: () => void; }>(null) const portraitRef = useRef<{ handleRefresh: () => void; }>(null) const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null) const habitsRef = useRef<{ handleRefresh: () => void; }>(null) + + useEffect(() => { + if (!id) return + implicitCheckData(id) + .then(res => { + if (!(res as { exists: boolean }).exists) { + message.warning(t('implicitDetail.noData')) + } + }) + }, [id]) const handleRefresh = () => { if (!id) { From 9675982555fa76b76a6d6946c3196bb72bc3b6d5 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 15:33:17 +0800 Subject: [PATCH 04/83] [changes] Implicit and emotional memories are stored in a database. --- api/app/cache/memory/emotion_memory.py | 134 ----------- api/app/cache/memory/implicit_memory.py | 136 ----------- api/app/celery_app.py | 16 +- api/app/controllers/emotion_controller.py | 114 ++++++---- .../controllers/implicit_memory_controller.py | 59 ++++- api/app/models/__init__.py | 4 +- .../models/implicit_emotions_storage_model.py | 46 ++++ .../implicit_emotions_storage_repository.py | 169 ++++++++++++++ api/app/services/emotion_analytics_service.py | 58 +++-- api/app/services/implicit_memory_service.py | 58 +++-- api/app/tasks.py | 211 +++++++++++++++++- 11 files changed, 607 insertions(+), 398 deletions(-) delete mode 100644 api/app/cache/memory/emotion_memory.py delete mode 100644 api/app/cache/memory/implicit_memory.py create mode 100644 api/app/models/implicit_emotions_storage_model.py create mode 100644 api/app/repositories/implicit_emotions_storage_repository.py diff --git a/api/app/cache/memory/emotion_memory.py b/api/app/cache/memory/emotion_memory.py deleted file mode 100644 index 45ea90de..00000000 --- a/api/app/cache/memory/emotion_memory.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -Emotion Suggestions Cache - -情绪个性化建议缓存模块 -用于缓存用户的情绪个性化建议数据 -""" -import json -import logging -from typing import Optional, Dict, Any -from datetime import datetime - -from app.aioRedis import aio_redis - -logger = logging.getLogger(__name__) - - -class EmotionMemoryCache: - """情绪建议缓存类""" - - # Key 前缀 - PREFIX = "cache:memory:emotion_memory" - - @classmethod - def _get_key(cls, *parts: str) -> str: - """生成 Redis key - - Args: - *parts: key 的各个部分 - - Returns: - 完整的 Redis key - """ - return ":".join([cls.PREFIX] + list(parts)) - - @classmethod - async def set_emotion_suggestions( - cls, - user_id: str, - suggestions_data: Dict[str, Any], - expire: int = 86400 - ) -> bool: - """设置用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - suggestions_data: 建议数据字典,包含: - - health_summary: 健康状态摘要 - - suggestions: 建议列表 - - generated_at: 生成时间(可选) - expire: 过期时间(秒),默认24小时(86400秒) - - Returns: - 是否设置成功 - """ - try: - key = cls._get_key("suggestions", user_id) - - # 添加生成时间戳 - if "generated_at" not in suggestions_data: - suggestions_data["generated_at"] = datetime.now().isoformat() - - # 添加缓存标记 - suggestions_data["cached"] = True - - value = json.dumps(suggestions_data, ensure_ascii=False) - await aio_redis.set(key, value, ex=expire) - logger.info(f"设置情绪建议缓存成功: {key}, 过期时间: {expire}秒") - return True - except Exception as e: - logger.error(f"设置情绪建议缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_emotion_suggestions(cls, user_id: str) -> Optional[Dict[str, Any]]: - """获取用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 建议数据字典,如果不存在或已过期返回 None - """ - try: - key = cls._get_key("suggestions", user_id) - value = await aio_redis.get(key) - - if value: - data = json.loads(value) - logger.info(f"成功获取情绪建议缓存: {key}") - return data - - logger.info(f"情绪建议缓存不存在或已过期: {key}") - return None - except Exception as e: - logger.error(f"获取情绪建议缓存失败: {e}", exc_info=True) - return None - - @classmethod - async def delete_emotion_suggestions(cls, user_id: str) -> bool: - """删除用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 是否删除成功 - """ - try: - key = cls._get_key("suggestions", user_id) - result = await aio_redis.delete(key) - logger.info(f"删除情绪建议缓存: {key}, 结果: {result}") - return result > 0 - except Exception as e: - logger.error(f"删除情绪建议缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_suggestions_ttl(cls, user_id: str) -> int: - """获取情绪建议缓存的剩余过期时间 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 剩余秒数,-1表示永不过期,-2表示key不存在 - """ - try: - key = cls._get_key("suggestions", user_id) - ttl = await aio_redis.ttl(key) - logger.debug(f"情绪建议缓存TTL: {key} = {ttl}秒") - return ttl - except Exception as e: - logger.error(f"获取情绪建议缓存TTL失败: {e}") - return -2 diff --git a/api/app/cache/memory/implicit_memory.py b/api/app/cache/memory/implicit_memory.py deleted file mode 100644 index 21f08e9a..00000000 --- a/api/app/cache/memory/implicit_memory.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Implicit Memory Profile Cache - -隐式记忆用户画像缓存模块 -用于缓存用户的完整画像数据(偏好标签、四维画像、兴趣领域、行为习惯) -""" -import json -import logging -from typing import Optional, Dict, Any -from datetime import datetime - -from app.aioRedis import aio_redis - -logger = logging.getLogger(__name__) - - -class ImplicitMemoryCache: - """隐式记忆用户画像缓存类""" - - # Key 前缀 - PREFIX = "cache:memory:implicit_memory" - - @classmethod - def _get_key(cls, *parts: str) -> str: - """生成 Redis key - - Args: - *parts: key 的各个部分 - - Returns: - 完整的 Redis key - """ - return ":".join([cls.PREFIX] + list(parts)) - - @classmethod - async def set_user_profile( - cls, - user_id: str, - profile_data: Dict[str, Any], - expire: int = 86400 - ) -> bool: - """设置用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - profile_data: 画像数据字典,包含: - - preferences: 偏好标签列表 - - portrait: 四维画像对象 - - interest_areas: 兴趣领域分布对象 - - habits: 行为习惯列表 - - generated_at: 生成时间(可选) - expire: 过期时间(秒),默认24小时(86400秒) - - Returns: - 是否设置成功 - """ - try: - key = cls._get_key("profile", user_id) - - # 添加生成时间戳 - if "generated_at" not in profile_data: - profile_data["generated_at"] = datetime.now().isoformat() - - # 添加缓存标记 - profile_data["cached"] = True - - value = json.dumps(profile_data, ensure_ascii=False) - await aio_redis.set(key, value, ex=expire) - logger.info(f"设置用户画像缓存成功: {key}, 过期时间: {expire}秒") - return True - except Exception as e: - logger.error(f"设置用户画像缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_user_profile(cls, user_id: str) -> Optional[Dict[str, Any]]: - """获取用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 画像数据字典,如果不存在或已过期返回 None - """ - try: - key = cls._get_key("profile", user_id) - value = await aio_redis.get(key) - - if value: - data = json.loads(value) - logger.info(f"成功获取用户画像缓存: {key}") - return data - - logger.info(f"用户画像缓存不存在或已过期: {key}") - return None - except Exception as e: - logger.error(f"获取用户画像缓存失败: {e}", exc_info=True) - return None - - @classmethod - async def delete_user_profile(cls, user_id: str) -> bool: - """删除用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 是否删除成功 - """ - try: - key = cls._get_key("profile", user_id) - result = await aio_redis.delete(key) - logger.info(f"删除用户画像缓存: {key}, 结果: {result}") - return result > 0 - except Exception as e: - logger.error(f"删除用户画像缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_profile_ttl(cls, user_id: str) -> int: - """获取用户画像缓存的剩余过期时间 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 剩余秒数,-1表示永不过期,-2表示key不存在 - """ - try: - key = cls._get_key("profile", user_id) - ttl = await aio_redis.ttl(key) - logger.debug(f"用户画像缓存TTL: {key} = {ttl}秒") - return ttl - except Exception as e: - logger.error(f"获取用户画像缓存TTL失败: {e}") - return -2 diff --git a/api/app/celery_app.py b/api/app/celery_app.py index 8ef44975..e804d303 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -82,7 +82,8 @@ celery_app.conf.update( 'app.tasks.workspace_reflection_task': {'queue': 'periodic_tasks'}, 'app.tasks.regenerate_memory_cache': {'queue': 'periodic_tasks'}, 'app.tasks.run_forgetting_cycle_task': {'queue': 'periodic_tasks'}, - 'app.controllers.memory_storage_controller.search_all': {'queue': 'periodic_tasks'}, + 'app.tasks.write_all_workspaces_memory_task': {'queue': 'periodic_tasks'}, + 'app.tasks.update_implicit_emotions_storage': {'queue': 'periodic_tasks'}, }, ) @@ -95,6 +96,7 @@ memory_cache_regeneration_schedule = timedelta(hours=settings.MEMORY_CACHE_REGEN # 这个30秒的设计不合理 workspace_reflection_schedule = timedelta(seconds=30) # 每30秒运行一次settings.REFLECTION_INTERVAL_TIME forgetting_cycle_schedule = timedelta(hours=24) # 每24小时运行一次遗忘周期 +implicit_emotions_update_schedule = timedelta(hours=24) # 每24小时更新一次隐性记忆和情绪数据 #构建定时任务配置 beat_schedule_config = { @@ -122,9 +124,13 @@ if settings.DEFAULT_WORKSPACE_ID: beat_schedule_config["write-total-memory"] = { "task": "app.controllers.memory_storage_controller.search_all", "schedule": memory_increment_schedule, - "kwargs": { - "workspace_id": settings.DEFAULT_WORKSPACE_ID, - }, - } + "args": (), + }, + "update-implicit-emotions-storage": { + "task": "app.tasks.update_implicit_emotions_storage", + "schedule": implicit_emotions_update_schedule, + "args": (), + }, +} celery_app.conf.beat_schedule = beat_schedule_config diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index eb2436d2..02ce7862 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -208,6 +208,57 @@ async def get_emotion_health( +@router.post("/check-data", response_model=ApiResponse) +async def check_emotion_data_exists( + request: EmotionSuggestionsRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """检查用户情绪建议数据是否存在 + + Args: + request: 包含 end_user_id + db: 数据库会话 + current_user: 当前用户 + + Returns: + 数据存在状态 + """ + try: + api_logger.info( + f"检查用户情绪建议数据是否存在: {request.end_user_id}", + extra={"end_user_id": request.end_user_id} + ) + + # 从数据库获取建议 + data = await emotion_service.get_cached_suggestions( + end_user_id=request.end_user_id, + db=db + ) + + if data is None: + api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") + return fail( + BizCode.NOT_FOUND, + "情绪建议数据不存在,请点击右上角刷新进行初始化", + {"exists": False} + ) + + api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") + return success(data={"exists": True}, msg="情绪建议数据已存在") + + except Exception as e: + api_logger.error( + f"检查情绪建议数据失败: {str(e)}", + extra={"end_user_id": request.end_user_id}, + exc_info=True + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"检查情绪建议数据失败: {str(e)}" + ) + + @router.post("/suggestions", response_model=ApiResponse) async def get_emotion_suggestions( request: EmotionSuggestionsRequest, @@ -215,7 +266,7 @@ async def get_emotion_suggestions( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """获取个性化情绪建议(从缓存读取) + """获取个性化情绪建议(从数据库读取) Args: request: 包含 end_user_id 和可选的 config_id @@ -223,77 +274,47 @@ async def get_emotion_suggestions( current_user: 当前用户 Returns: - 缓存的个性化情绪建议响应 + 存储的个性化情绪建议响应 """ try: # 使用集中化的语言校验 language = get_language_from_header(language_type) api_logger.info( - f"用户 {current_user.username} 请求获取个性化情绪建议(缓存)", + f"用户 {current_user.username} 请求获取个性化情绪建议", extra={ "end_user_id": request.end_user_id, "config_id": request.config_id } ) - # 从缓存获取建议 + # 从数据库获取建议 data = await emotion_service.get_cached_suggestions( end_user_id=request.end_user_id, db=db ) if data is None: - # 缓存不存在或已过期,自动触发生成 + # 数据不存在,返回提示信息 api_logger.info( - f"用户 {request.end_user_id} 的建议缓存不存在或已过期,自动生成新建议", + f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - try: - data = await emotion_service.generate_emotion_suggestions( - end_user_id=request.end_user_id, - db=db, - language=language - ) - # 保存到缓存 - await emotion_service.save_suggestions_cache( - end_user_id=request.end_user_id, - suggestions_data=data, - db=db, - expires_hours=24 - ) - except (ValueError, KeyError) as gen_e: - # 预期内的业务异常:配置缺失、数据格式问题等 - api_logger.warning( - f"自动生成建议失败(业务异常): {str(gen_e)}", - extra={"end_user_id": request.end_user_id} - ) - return fail( - BizCode.NOT_FOUND, - f"自动生成建议失败: {str(gen_e)}", - "" - ) - except Exception as gen_e: - # 非预期异常:记录完整 traceback 便于排查 - api_logger.error( - f"自动生成建议时发生未预期异常: {str(gen_e)}", - extra={"end_user_id": request.end_user_id}, - exc_info=True - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"生成建议时发生内部错误: {str(gen_e)}" - ) + return fail( + BizCode.NOT_FOUND, + "情绪建议数据不存在,请点击右上角刷新进行初始化", + "" + ) api_logger.info( - "个性化建议获取成功(缓存)", + "个性化建议获取成功", extra={ "end_user_id": request.end_user_id, "suggestions_count": len(data.get("suggestions", [])) } ) - return success(data=data, msg="个性化建议获取成功(缓存)") + return success(data=data, msg="个性化建议获取成功") except Exception as e: api_logger.error( @@ -314,7 +335,7 @@ async def generate_emotion_suggestions( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """生成个性化情绪建议(调用LLM并缓存) + """生成个性化情绪建议(调用LLM并保存到数据库) Args: request: 包含 end_user_id @@ -342,12 +363,11 @@ async def generate_emotion_suggestions( language=language ) - # 保存到缓存 + # 保存到数据库 await emotion_service.save_suggestions_cache( end_user_id=request.end_user_id, suggestions_data=data, - db=db, - expires_hours=24 + db=db ) api_logger.info( diff --git a/api/app/controllers/implicit_memory_controller.py b/api/app/controllers/implicit_memory_controller.py index 96e437d6..91e634c9 100644 --- a/api/app/controllers/implicit_memory_controller.py +++ b/api/app/controllers/implicit_memory_controller.py @@ -122,6 +122,49 @@ def validate_confidence_threshold(threshold: float) -> None: raise ValueError("confidence_threshold must be between 0.0 and 1.0") +@router.get("/check-data/{end_user_id}", response_model=ApiResponse) +@cur_workspace_access_guard() +async def check_user_data_exists( + end_user_id: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +) -> ApiResponse: + """ + 检查用户画像数据是否存在 + + Args: + end_user_id: 目标用户ID + + Returns: + 数据存在状态 + """ + api_logger.info(f"检查用户画像数据是否存在: {end_user_id}") + + try: + # Validate inputs + validate_user_id(end_user_id) + + # Create service with user-specific config + service = ImplicitMemoryService(db=db, end_user_id=end_user_id) + + # Get cached profile + cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) + + if cached_profile is None: + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") + return fail( + BizCode.NOT_FOUND, + "画像数据不存在,请点击右上角刷新进行初始化", + {"exists": False} + ) + + api_logger.info(f"用户 {end_user_id} 的画像数据存在") + return success(data={"exists": True}, msg="画像数据已存在") + + except Exception as e: + return handle_implicit_memory_error(e, "检查画像数据", end_user_id) + + @router.get("/preferences/{end_user_id}", response_model=ApiResponse) @cur_workspace_access_guard() async def get_preference_tags( @@ -159,10 +202,10 @@ async def get_preference_tags( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -230,10 +273,10 @@ async def get_dimension_portrait( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -278,10 +321,10 @@ async def get_interest_area_distribution( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -330,10 +373,10 @@ async def get_behavior_habits( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index b1b723e9..c6098a6d 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -35,6 +35,7 @@ from .ontology_scene import OntologyScene from .ontology_class import OntologyClass from .ontology_scene import OntologyScene from .ontology_class import OntologyClass +from .implicit_emotions_storage_model import ImplicitEmotionsStorage __all__ = [ "Tenants", @@ -90,5 +91,6 @@ __all__ = [ "MemoryPerceptualModel", "ModelBase", "LoadBalanceStrategy", - "Skill" + "Skill", + "ImplicitEmotionsStorage" ] diff --git a/api/app/models/implicit_emotions_storage_model.py b/api/app/models/implicit_emotions_storage_model.py new file mode 100644 index 00000000..57c0fd61 --- /dev/null +++ b/api/app/models/implicit_emotions_storage_model.py @@ -0,0 +1,46 @@ +""" +Implicit Emotions Storage Model + +数据库模型:存储用户的隐性记忆画像和情绪建议数据 +替代原有的Redis缓存方式 +""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, DateTime, Index +from sqlalchemy.dialects.postgresql import UUID, JSONB +from app.db import Base + + +class ImplicitEmotionsStorage(Base): + """隐性记忆和情绪存储表""" + + __tablename__ = "implicit_emotions_storage" + + # 主键 + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="主键ID") + + # 用户标识 + end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID") + + # 隐性记忆画像数据(JSON格式) + implicit_profile = Column(JSONB, nullable=True, comment="隐性记忆用户画像数据") + + # 情绪建议数据(JSON格式) + emotion_suggestions = Column(JSONB, nullable=True, comment="情绪个性化建议数据") + + # 时间戳 + created_at = Column(DateTime, nullable=False, default=datetime.utcnow, comment="创建时间") + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间") + + # 数据生成时间(用于业务逻辑) + implicit_generated_at = Column(DateTime, nullable=True, comment="隐性记忆画像生成时间") + emotion_generated_at = Column(DateTime, nullable=True, comment="情绪建议生成时间") + + # 索引 + __table_args__ = ( + Index('idx_end_user_id', 'end_user_id'), + Index('idx_updated_at', 'updated_at'), + ) + + def __repr__(self): + return f"" diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py new file mode 100644 index 00000000..fd4b10ce --- /dev/null +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -0,0 +1,169 @@ +""" +Implicit Emotions Storage Repository + +数据访问层:处理隐性记忆和情绪数据的数据库操作 +""" +import logging +from datetime import datetime +from typing import Optional, List +from sqlalchemy.orm import Session +from sqlalchemy import select + +from app.models.implicit_emotions_storage_model import ImplicitEmotionsStorage + +logger = logging.getLogger(__name__) + + +class ImplicitEmotionsStorageRepository: + """隐性记忆和情绪存储仓储类""" + + def __init__(self, db: Session): + self.db = db + + def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitEmotionsStorage]: + """根据终端用户ID获取存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 存储记录,如果不存在返回None + """ + try: + stmt = select(ImplicitEmotionsStorage).where( + ImplicitEmotionsStorage.end_user_id == end_user_id + ) + result = self.db.execute(stmt).scalar_one_or_none() + return result + except Exception as e: + logger.error(f"获取用户存储记录失败: end_user_id={end_user_id}, error={e}") + return None + + def create(self, end_user_id: str) -> ImplicitEmotionsStorage: + """创建新的存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 新创建的存储记录 + """ + try: + storage = ImplicitEmotionsStorage( + end_user_id=end_user_id, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + self.db.add(storage) + self.db.commit() + self.db.refresh(storage) + logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"创建用户存储记录失败: end_user_id={end_user_id}, error={e}") + raise + + def update_implicit_profile( + self, + end_user_id: str, + profile_data: dict + ) -> Optional[ImplicitEmotionsStorage]: + """更新隐性记忆画像数据 + + Args: + end_user_id: 终端用户ID + profile_data: 画像数据 + + Returns: + 更新后的存储记录 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + + if storage is None: + # 如果记录不存在,创建新记录 + storage = self.create(end_user_id) + + storage.implicit_profile = profile_data + storage.implicit_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(storage) + logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"更新隐性记忆画像失败: end_user_id={end_user_id}, error={e}") + raise + + def update_emotion_suggestions( + self, + end_user_id: str, + suggestions_data: dict + ) -> Optional[ImplicitEmotionsStorage]: + """更新情绪建议数据 + + Args: + end_user_id: 终端用户ID + suggestions_data: 建议数据 + + Returns: + 更新后的存储记录 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + + if storage is None: + # 如果记录不存在,创建新记录 + storage = self.create(end_user_id) + + storage.emotion_suggestions = suggestions_data + storage.emotion_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(storage) + logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"更新情绪建议失败: end_user_id={end_user_id}, error={e}") + raise + + def get_all_user_ids(self) -> List[str]: + """获取所有已存储数据的用户ID列表 + + Returns: + 用户ID列表 + """ + try: + stmt = select(ImplicitEmotionsStorage.end_user_id) + result = self.db.execute(stmt).scalars().all() + return list(result) + except Exception as e: + logger.error(f"获取所有用户ID失败: error={e}") + return [] + + def delete_by_end_user_id(self, end_user_id: str) -> bool: + """删除用户的存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 是否删除成功 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + if storage: + self.db.delete(storage) + self.db.commit() + logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") + return True + return False + except Exception as e: + self.db.rollback() + logger.error(f"删除用户存储记录失败: end_user_id={end_user_id}, error={e}") + return False diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index 89e3cab9..099cbfb7 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -843,32 +843,33 @@ class EmotionAnalyticsService: end_user_id: str, db: Session, ) -> Optional[Dict[str, Any]]: - """从 Redis 缓存获取个性化情绪建议 + """从数据库获取个性化情绪建议 Args: end_user_id: 宿主ID(用户组ID) - db: 数据库会话(保留参数以保持接口兼容性) + db: 数据库会话 Returns: - Dict: 缓存的建议数据,如果不存在或已过期返回 None + Dict: 存储的建议数据,如果不存在返回 None """ try: - from app.cache.memory.emotion_memory import EmotionMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}") + logger.info(f"尝试从数据库获取情绪建议: user={end_user_id}") - # 从 Redis 获取缓存 - cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id) + # 从数据库获取存储记录 + repo = ImplicitEmotionsStorageRepository(db) + storage = repo.get_by_end_user_id(end_user_id) - if cached_data is None: - logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期") + if storage is None or storage.emotion_suggestions is None: + logger.info(f"用户 {end_user_id} 的建议数据不存在") return None - logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}") - return cached_data + logger.info(f"成功从数据库获取建议: user={end_user_id}") + return storage.emotion_suggestions except Exception as e: - logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True) + logger.error(f"从数据库获取建议失败: {str(e)}", exc_info=True) return None async def save_suggestions_cache( @@ -876,36 +877,27 @@ class EmotionAnalyticsService: end_user_id: str, suggestions_data: Dict[str, Any], db: Session, - expires_hours: int = 24 + expires_hours: int = 24 # 参数保留以保持接口兼容性 ) -> None: - """保存建议到 Redis 缓存 + """保存建议到数据库 Args: end_user_id: 宿主ID(用户组ID) suggestions_data: 建议数据 - db: 数据库会话(保留参数以保持接口兼容性) - expires_hours: 过期时间(小时),默认24小时 + db: 数据库会话 + expires_hours: 保留参数(兼容性) """ try: - from app.cache.memory.emotion_memory import EmotionMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时") + logger.info(f"保存建议到数据库: user={end_user_id}") - # 计算过期时间(秒) - expire_seconds = expires_hours * 3600 + # 保存到数据库 + repo = ImplicitEmotionsStorageRepository(db) + repo.update_emotion_suggestions(end_user_id, suggestions_data) - # 保存到 Redis - success = await EmotionMemoryCache.set_emotion_suggestions( - user_id=end_user_id, - suggestions_data=suggestions_data, - expire=expire_seconds - ) - - if success: - logger.info(f"建议缓存保存成功: user={end_user_id}") - else: - logger.warning(f"建议缓存保存失败: user={end_user_id}") + logger.info(f"建议保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True) - # 不抛出异常,缓存失败不应影响主流程 \ No newline at end of file + logger.error(f"保存建议失败: {str(e)}", exc_info=True) + # 不抛出异常,存储失败不应影响主流程 \ No newline at end of file diff --git a/api/app/services/implicit_memory_service.py b/api/app/services/implicit_memory_service.py index 34ebe880..534f138c 100644 --- a/api/app/services/implicit_memory_service.py +++ b/api/app/services/implicit_memory_service.py @@ -422,32 +422,33 @@ class ImplicitMemoryService: end_user_id: str, db: Session ) -> Optional[dict]: - """从 Redis 缓存获取完整用户画像 + """从数据库获取完整用户画像 Args: end_user_id: 终端用户ID - db: 数据库会话(保留参数以保持接口兼容性) + db: 数据库会话 Returns: - Dict: 缓存的画像数据,如果不存在或已过期返回 None + Dict: 存储的画像数据,如果不存在返回 None """ try: - from app.cache.memory.implicit_memory import ImplicitMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"尝试从 Redis 缓存获取用户画像: user={end_user_id}") + logger.info(f"尝试从数据库获取用户画像: user={end_user_id}") - # 从 Redis 获取缓存 - cached_data = await ImplicitMemoryCache.get_user_profile(end_user_id) + # 从数据库获取存储记录 + repo = ImplicitEmotionsStorageRepository(db) + storage = repo.get_by_end_user_id(end_user_id) - if cached_data is None: - logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + if storage is None or storage.implicit_profile is None: + logger.info(f"用户 {end_user_id} 的画像数据不存在") return None - logger.info(f"成功从 Redis 缓存获取用户画像: user={end_user_id}") - return cached_data + logger.info(f"成功从数据库获取用户画像: user={end_user_id}") + return storage.implicit_profile except Exception as e: - logger.error(f"从 Redis 缓存获取用户画像失败: {str(e)}", exc_info=True) + logger.error(f"从数据库获取用户画像失败: {str(e)}", exc_info=True) return None async def save_profile_cache( @@ -455,36 +456,27 @@ class ImplicitMemoryService: end_user_id: str, profile_data: dict, db: Session, - expires_hours: int = 168 # 默认7天 + expires_hours: int = 168 # 参数保留以保持接口兼容性 ) -> None: - """保存用户画像到 Redis 缓存 + """保存用户画像到数据库 Args: end_user_id: 终端用户ID profile_data: 画像数据 - db: 数据库会话(保留参数以保持接口兼容性) - expires_hours: 过期时间(小时),默认168小时(7天) + db: 数据库会话 + expires_hours: 保留参数(兼容性) """ try: - from app.cache.memory.implicit_memory import ImplicitMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"保存用户画像到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时") + logger.info(f"保存用户画像到数据库: user={end_user_id}") - # 计算过期时间(秒) - expire_seconds = expires_hours * 3600 + # 保存到数据库 + repo = ImplicitEmotionsStorageRepository(db) + repo.update_implicit_profile(end_user_id, profile_data) - # 保存到 Redis - success = await ImplicitMemoryCache.set_user_profile( - user_id=end_user_id, - profile_data=profile_data, - expire=expire_seconds - ) - - if success: - logger.info(f"用户画像缓存保存成功: user={end_user_id}") - else: - logger.warning(f"用户画像缓存保存失败: user={end_user_id}") + logger.info(f"用户画像保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True) - # 不抛出异常,缓存失败不应影响主流程 + logger.error(f"保存用户画像失败: {str(e)}", exc_info=True) + # 不抛出异常,存储失败不应影响主流程 diff --git a/api/app/tasks.py b/api/app/tasks.py index d408a0da..5a320c3f 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -1924,4 +1924,213 @@ def run_forgetting_cycle_task(self, config_id: Optional[uuid.UUID] = None) -> Di # "config_id": config_id, # "elapsed_time": elapsed_time, # "task_id": self.request.id -# } \ No newline at end of file +# } + + +# ============================================================================= +# 隐性记忆和情绪数据更新定时任务 +# ============================================================================= + +@celery_app.task( + name="app.tasks.update_implicit_emotions_storage", + bind=True, + ignore_result=True, + max_retries=0, + acks_late=False, + time_limit=7200, # 2小时硬超时 + soft_time_limit=6900, # 1小时55分钟软超时 +) +def update_implicit_emotions_storage(self) -> Dict[str, Any]: + """定时任务:更新所有用户的隐性记忆画像和情绪建议数据 + + 遍历数据库中所有已存在数据的用户,为每个用户重新生成隐性记忆画像和情绪建议。 + 实现错误隔离,单个用户失败不影响其他用户的处理。 + + Returns: + 包含任务执行结果的字典,包括: + - status: 任务状态 (SUCCESS/FAILURE) + - message: 执行消息 + - total_users: 总用户数 + - successful_implicit: 成功更新隐性记忆的用户数 + - successful_emotion: 成功更新情绪建议的用户数 + - failed: 失败的用户数 + - user_results: 每个用户的详细结果 + - elapsed_time: 执行耗时(秒) + - task_id: 任务ID + """ + start_time = time.time() + + async def _run() -> Dict[str, Any]: + from app.core.logging_config import get_logger + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository + from app.services.implicit_memory_service import ImplicitMemoryService + from app.services.emotion_analytics_service import EmotionAnalyticsService + + logger = get_logger(__name__) + logger.info("开始执行隐性记忆和情绪数据更新定时任务") + + total_users = 0 + successful_implicit = 0 + successful_emotion = 0 + failed = 0 + user_results = [] + + with get_db_context() as db: + try: + # 获取所有已存储数据的用户ID + repo = ImplicitEmotionsStorageRepository(db) + user_ids = repo.get_all_user_ids() + total_users = len(user_ids) + + logger.info(f"找到 {total_users} 个需要更新的用户") + + # 遍历每个用户并更新数据 + for end_user_id in user_ids: + logger.info(f"开始处理用户: {end_user_id}") + user_start_time = time.time() + + implicit_success = False + emotion_success = False + errors = [] + + try: + # 更新隐性记忆画像 + try: + implicit_service = ImplicitMemoryService(db=db, end_user_id=end_user_id) + profile_data = await implicit_service.generate_complete_profile(user_id=end_user_id) + await implicit_service.save_profile_cache( + end_user_id=end_user_id, + profile_data=profile_data, + db=db + ) + implicit_success = True + logger.info(f"成功更新用户 {end_user_id} 的隐性记忆画像") + except Exception as e: + error_msg = f"隐性记忆更新失败: {str(e)}" + errors.append(error_msg) + logger.error(f"用户 {end_user_id} {error_msg}") + + # 更新情绪建议 + try: + emotion_service = EmotionAnalyticsService(db=db, end_user_id=end_user_id) + suggestions_data = await emotion_service.generate_emotion_suggestions( + end_user_id=end_user_id, + db=db, + language="zh" + ) + await emotion_service.save_suggestions_cache( + end_user_id=end_user_id, + suggestions_data=suggestions_data, + db=db + ) + emotion_success = True + logger.info(f"成功更新用户 {end_user_id} 的情绪建议") + except Exception as e: + error_msg = f"情绪建议更新失败: {str(e)}" + errors.append(error_msg) + logger.error(f"用户 {end_user_id} {error_msg}") + + # 统计结果 + if implicit_success: + successful_implicit += 1 + if emotion_success: + successful_emotion += 1 + if not implicit_success and not emotion_success: + failed += 1 + + user_elapsed = time.time() - user_start_time + + # 记录用户处理结果 + user_result = { + "end_user_id": end_user_id, + "implicit_success": implicit_success, + "emotion_success": emotion_success, + "errors": errors, + "elapsed_time": user_elapsed + } + user_results.append(user_result) + + logger.info( + f"用户 {end_user_id} 处理完成: " + f"隐性记忆={'成功' if implicit_success else '失败'}, " + f"情绪建议={'成功' if emotion_success else '失败'}, " + f"耗时={user_elapsed:.2f}秒" + ) + + except Exception as e: + # 单个用户失败不影响其他用户(错误隔离) + failed += 1 + user_elapsed = time.time() - user_start_time + error_info = { + "end_user_id": end_user_id, + "implicit_success": False, + "emotion_success": False, + "errors": [str(e)], + "elapsed_time": user_elapsed + } + user_results.append(error_info) + logger.error(f"处理用户 {end_user_id} 时出错: {str(e)}") + + # 记录总体统计信息 + logger.info( + f"隐性记忆和情绪数据更新定时任务完成: " + f"总用户数={total_users}, " + f"隐性记忆成功={successful_implicit}, " + f"情绪建议成功={successful_emotion}, " + f"失败={failed}" + ) + + return { + "status": "SUCCESS", + "message": f"成功处理 {total_users} 个用户,隐性记忆 {successful_implicit} 个成功,情绪建议 {successful_emotion} 个成功", + "total_users": total_users, + "successful_implicit": successful_implicit, + "successful_emotion": successful_emotion, + "failed": failed, + "user_results": user_results[:50] # 只保留前50个用户的详细结果 + } + + except Exception as e: + logger.error(f"隐性记忆和情绪数据更新定时任务执行失败: {str(e)}") + return { + "status": "FAILURE", + "error": str(e), + "total_users": total_users, + "successful_implicit": successful_implicit, + "successful_emotion": successful_emotion, + "failed": failed, + "user_results": user_results[:50] + } + + try: + # 使用 nest_asyncio 来避免事件循环冲突 + try: + import nest_asyncio + nest_asyncio.apply() + except ImportError: + pass + + # 尝试获取现有事件循环,如果不存在则创建新的 + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + result = loop.run_until_complete(_run()) + elapsed_time = time.time() - start_time + result["elapsed_time"] = elapsed_time + result["task_id"] = self.request.id + + return result + except Exception as e: + elapsed_time = time.time() - start_time + return { + "status": "FAILURE", + "error": str(e), + "elapsed_time": elapsed_time, + "task_id": self.request.id + } From 006c6cd1595a33f549b4bb8d94969efba1490204 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 15:16:47 +0800 Subject: [PATCH 05/83] [changes] AI reviews and modifies the code --- api/app/cache/__init__.py | 7 +- api/app/cache/memory/__init__.py | 8 +- api/app/controllers/emotion_controller.py | 12 +- .../controllers/implicit_memory_controller.py | 31 +-- .../models/implicit_emotions_storage_model.py | 7 +- .../implicit_emotions_storage_repository.py | 212 +++++++----------- api/app/services/emotion_analytics_service.py | 6 +- api/app/services/implicit_memory_service.py | 4 +- api/app/tasks.py | 15 +- 9 files changed, 114 insertions(+), 188 deletions(-) diff --git a/api/app/cache/__init__.py b/api/app/cache/__init__.py index a79d4cb2..748ce8ae 100644 --- a/api/app/cache/__init__.py +++ b/api/app/cache/__init__.py @@ -2,10 +2,7 @@ Cache 缓存模块 提供各种缓存功能的统一入口 +注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .memory import EmotionMemoryCache, ImplicitMemoryCache -__all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", -] +__all__ = [] diff --git a/api/app/cache/memory/__init__.py b/api/app/cache/memory/__init__.py index 4ada3153..35f45aad 100644 --- a/api/app/cache/memory/__init__.py +++ b/api/app/cache/memory/__init__.py @@ -2,11 +2,7 @@ Memory 缓存模块 提供记忆系统相关的缓存功能 +注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .emotion_memory import EmotionMemoryCache -from .implicit_memory import ImplicitMemoryCache -__all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", -] +__all__ = [] diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 02ce7862..0a8b5fc8 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -262,7 +262,6 @@ async def check_emotion_data_exists( @router.post("/suggestions", response_model=ApiResponse) async def get_emotion_suggestions( request: EmotionSuggestionsRequest, - language_type: str = Header(default=None, alias="X-Language-Type"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): @@ -277,9 +276,6 @@ async def get_emotion_suggestions( 存储的个性化情绪建议响应 """ try: - # 使用集中化的语言校验 - language = get_language_from_header(language_type) - api_logger.info( f"用户 {current_user.username} 请求获取个性化情绪建议", extra={ @@ -295,15 +291,13 @@ async def get_emotion_suggestions( ) if data is None: - # 数据不存在,返回提示信息 api_logger.info( f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - return fail( - BizCode.NOT_FOUND, - "情绪建议数据不存在,请点击右上角刷新进行初始化", - "" + return success( + data={"exists": False}, + msg="情绪建议数据不存在,请点击右上角刷新进行初始化" ) api_logger.info( diff --git a/api/app/controllers/implicit_memory_controller.py b/api/app/controllers/implicit_memory_controller.py index 91e634c9..76a87c5f 100644 --- a/api/app/controllers/implicit_memory_controller.py +++ b/api/app/controllers/implicit_memory_controller.py @@ -152,10 +152,9 @@ async def check_user_data_exists( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - {"exists": False} + return success( + data={"exists": False}, + msg="画像数据不存在,请点击右上角刷新进行初始化" ) api_logger.info(f"用户 {end_user_id} 的画像数据存在") @@ -203,11 +202,7 @@ async def get_preference_tags( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract preferences from cache preferences = cached_profile.get("preferences", []) @@ -274,11 +269,7 @@ async def get_dimension_portrait( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract portrait from cache portrait = cached_profile.get("portrait", {}) @@ -322,11 +313,7 @@ async def get_interest_area_distribution( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract interest areas from cache interest_areas = cached_profile.get("interest_areas", {}) @@ -374,11 +361,7 @@ async def get_behavior_habits( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract habits from cache habits = cached_profile.get("habits", []) diff --git a/api/app/models/implicit_emotions_storage_model.py b/api/app/models/implicit_emotions_storage_model.py index 57c0fd61..cf654950 100644 --- a/api/app/models/implicit_emotions_storage_model.py +++ b/api/app/models/implicit_emotions_storage_model.py @@ -19,8 +19,8 @@ class ImplicitEmotionsStorage(Base): # 主键 id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="主键ID") - # 用户标识 - end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID") + # 用户标识(unique=True会自动创建唯一索引) + end_user_id = Column(String(255), nullable=False, unique=True, comment="终端用户ID") # 隐性记忆画像数据(JSON格式) implicit_profile = Column(JSONB, nullable=True, comment="隐性记忆用户画像数据") @@ -36,9 +36,8 @@ class ImplicitEmotionsStorage(Base): implicit_generated_at = Column(DateTime, nullable=True, comment="隐性记忆画像生成时间") emotion_generated_at = Column(DateTime, nullable=True, comment="情绪建议生成时间") - # 索引 + # 索引(只为updated_at创建索引,end_user_id的unique约束已自动创建索引) __table_args__ = ( - Index('idx_end_user_id', 'end_user_id'), Index('idx_updated_at', 'updated_at'), ) diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py index fd4b10ce..176012b7 100644 --- a/api/app/repositories/implicit_emotions_storage_repository.py +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -2,10 +2,11 @@ Implicit Emotions Storage Repository 数据访问层:处理隐性记忆和情绪数据的数据库操作 +事务由调用方控制,仓储层只使用 flush/refresh """ import logging from datetime import datetime -from typing import Optional, List +from typing import Optional, Generator from sqlalchemy.orm import Session from sqlalchemy import select @@ -16,154 +17,105 @@ logger = logging.getLogger(__name__) class ImplicitEmotionsStorageRepository: """隐性记忆和情绪存储仓储类""" - + def __init__(self, db: Session): self.db = db - + def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitEmotionsStorage]: - """根据终端用户ID获取存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 存储记录,如果不存在返回None - """ + """根据终端用户ID获取存储记录""" try: stmt = select(ImplicitEmotionsStorage).where( ImplicitEmotionsStorage.end_user_id == end_user_id ) - result = self.db.execute(stmt).scalar_one_or_none() - return result + return self.db.execute(stmt).scalar_one_or_none() except Exception as e: logger.error(f"获取用户存储记录失败: end_user_id={end_user_id}, error={e}") return None - + def create(self, end_user_id: str) -> ImplicitEmotionsStorage: - """创建新的存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 新创建的存储记录 - """ - try: - storage = ImplicitEmotionsStorage( - end_user_id=end_user_id, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow() - ) - self.db.add(storage) - self.db.commit() - self.db.refresh(storage) - logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"创建用户存储记录失败: end_user_id={end_user_id}, error={e}") - raise - + """创建新的存储记录(事务由调用方提交)""" + storage = ImplicitEmotionsStorage( + end_user_id=end_user_id, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + self.db.add(storage) + self.db.flush() + self.db.refresh(storage) + logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") + return storage + def update_implicit_profile( self, end_user_id: str, profile_data: dict - ) -> Optional[ImplicitEmotionsStorage]: - """更新隐性记忆画像数据 - - Args: - end_user_id: 终端用户ID - profile_data: 画像数据 - - Returns: - 更新后的存储记录 - """ - try: - storage = self.get_by_end_user_id(end_user_id) - - if storage is None: - # 如果记录不存在,创建新记录 - storage = self.create(end_user_id) - - storage.implicit_profile = profile_data - storage.implicit_generated_at = datetime.utcnow() - storage.updated_at = datetime.utcnow() - - self.db.commit() - self.db.refresh(storage) - logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"更新隐性记忆画像失败: end_user_id={end_user_id}, error={e}") - raise - + ) -> ImplicitEmotionsStorage: + """更新隐性记忆画像数据(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage is None: + storage = self.create(end_user_id) + + storage.implicit_profile = profile_data + storage.implicit_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.flush() + self.db.refresh(storage) + logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") + return storage + def update_emotion_suggestions( self, end_user_id: str, suggestions_data: dict - ) -> Optional[ImplicitEmotionsStorage]: - """更新情绪建议数据 - + ) -> ImplicitEmotionsStorage: + """更新情绪建议数据(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage is None: + storage = self.create(end_user_id) + + storage.emotion_suggestions = suggestions_data + storage.emotion_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.flush() + self.db.refresh(storage) + logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") + return storage + + def get_all_user_ids(self, batch_size: int = 100) -> Generator[str, None, None]: + """分批次获取所有已存储数据的用户ID(避免大数据量内存溢出) + Args: - end_user_id: 终端用户ID - suggestions_data: 建议数据 - - Returns: - 更新后的存储记录 + batch_size: 每批次加载的数量,默认100 + + Yields: + 用户ID字符串 """ - try: - storage = self.get_by_end_user_id(end_user_id) - - if storage is None: - # 如果记录不存在,创建新记录 - storage = self.create(end_user_id) - - storage.emotion_suggestions = suggestions_data - storage.emotion_generated_at = datetime.utcnow() - storage.updated_at = datetime.utcnow() - - self.db.commit() - self.db.refresh(storage) - logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"更新情绪建议失败: end_user_id={end_user_id}, error={e}") - raise - - def get_all_user_ids(self) -> List[str]: - """获取所有已存储数据的用户ID列表 - - Returns: - 用户ID列表 - """ - try: - stmt = select(ImplicitEmotionsStorage.end_user_id) - result = self.db.execute(stmt).scalars().all() - return list(result) - except Exception as e: - logger.error(f"获取所有用户ID失败: error={e}") - return [] - + offset = 0 + while True: + try: + stmt = ( + select(ImplicitEmotionsStorage.end_user_id) + .order_by(ImplicitEmotionsStorage.end_user_id) + .limit(batch_size) + .offset(offset) + ) + batch = self.db.execute(stmt).scalars().all() + if not batch: + break + yield from batch + offset += batch_size + except Exception as e: + logger.error(f"分批获取用户ID失败: offset={offset}, error={e}") + break + def delete_by_end_user_id(self, end_user_id: str) -> bool: - """删除用户的存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 是否删除成功 - """ - try: - storage = self.get_by_end_user_id(end_user_id) - if storage: - self.db.delete(storage) - self.db.commit() - logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") - return True - return False - except Exception as e: - self.db.rollback() - logger.error(f"删除用户存储记录失败: end_user_id={end_user_id}, error={e}") - return False + """删除用户的存储记录(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage: + self.db.delete(storage) + self.db.flush() + logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") + return True + return False diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index 099cbfb7..c226348e 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -892,12 +892,12 @@ class EmotionAnalyticsService: logger.info(f"保存建议到数据库: user={end_user_id}") - # 保存到数据库 repo = ImplicitEmotionsStorageRepository(db) repo.update_emotion_suggestions(end_user_id, suggestions_data) + db.commit() logger.info(f"建议保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存建议失败: {str(e)}", exc_info=True) - # 不抛出异常,存储失败不应影响主流程 \ No newline at end of file + db.rollback() + logger.error(f"保存建议失败: {str(e)}", exc_info=True) \ No newline at end of file diff --git a/api/app/services/implicit_memory_service.py b/api/app/services/implicit_memory_service.py index 534f138c..4bd11deb 100644 --- a/api/app/services/implicit_memory_service.py +++ b/api/app/services/implicit_memory_service.py @@ -471,12 +471,12 @@ class ImplicitMemoryService: logger.info(f"保存用户画像到数据库: user={end_user_id}") - # 保存到数据库 repo = ImplicitEmotionsStorageRepository(db) repo.update_implicit_profile(end_user_id, profile_data) + db.commit() logger.info(f"用户画像保存成功: user={end_user_id}") except Exception as e: + db.rollback() logger.error(f"保存用户画像失败: {str(e)}", exc_info=True) - # 不抛出异常,存储失败不应影响主流程 diff --git a/api/app/tasks.py b/api/app/tasks.py index 5a320c3f..1675f25d 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -1963,6 +1963,8 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: async def _run() -> Dict[str, Any]: from app.core.logging_config import get_logger from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository + from app.models.implicit_emotions_storage_model import ImplicitEmotionsStorage + from sqlalchemy import select, func from app.services.implicit_memory_service import ImplicitMemoryService from app.services.emotion_analytics_service import EmotionAnalyticsService @@ -1977,15 +1979,18 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: with get_db_context() as db: try: - # 获取所有已存储数据的用户ID + # 获取所有已存储数据的用户ID(分批次处理) repo = ImplicitEmotionsStorageRepository(db) - user_ids = repo.get_all_user_ids() - total_users = len(user_ids) + # 先统计总数用于日志 + from sqlalchemy import func + total_users = db.execute( + select(func.count()).select_from(ImplicitEmotionsStorage) + ).scalar() or 0 logger.info(f"找到 {total_users} 个需要更新的用户") - # 遍历每个用户并更新数据 - for end_user_id in user_ids: + # 遍历每个用户并更新数据(分批次,避免一次性加载所有ID) + for end_user_id in repo.get_all_user_ids(batch_size=100): logger.info(f"开始处理用户: {end_user_id}") user_start_time = time.time() From 5edf3f2b8a698056ed1a17dc7ea2562c31888c9c Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 16:16:16 +0800 Subject: [PATCH 06/83] [changes] Test the scheduled task --- api/app/celery_app.py | 8 +-- api/app/controllers/emotion_controller.py | 88 +++++++++++------------ redbear-mem-benchmark | 2 +- 3 files changed, 47 insertions(+), 51 deletions(-) diff --git a/api/app/celery_app.py b/api/app/celery_app.py index e804d303..33fa1703 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -117,12 +117,8 @@ beat_schedule_config = { "config_id": None, # 使用默认配置,可以通过环境变量配置 }, }, -} - -#如果配置了默认工作空间ID,则添加记忆总量统计任务 -if settings.DEFAULT_WORKSPACE_ID: - beat_schedule_config["write-total-memory"] = { - "task": "app.controllers.memory_storage_controller.search_all", + "write-all-workspaces-memory": { + "task": "app.tasks.write_all_workspaces_memory_task", "schedule": memory_increment_schedule, "args": (), }, diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 0a8b5fc8..8cfc5014 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -208,55 +208,55 @@ async def get_emotion_health( -@router.post("/check-data", response_model=ApiResponse) -async def check_emotion_data_exists( - request: EmotionSuggestionsRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """检查用户情绪建议数据是否存在 +# @router.post("/check-data", response_model=ApiResponse) +# async def check_emotion_data_exists( +# request: EmotionSuggestionsRequest, +# db: Session = Depends(get_db), +# current_user: User = Depends(get_current_user), +# ): +# """检查用户情绪建议数据是否存在 - Args: - request: 包含 end_user_id - db: 数据库会话 - current_user: 当前用户 +# Args: +# request: 包含 end_user_id +# db: 数据库会话 +# current_user: 当前用户 - Returns: - 数据存在状态 - """ - try: - api_logger.info( - f"检查用户情绪建议数据是否存在: {request.end_user_id}", - extra={"end_user_id": request.end_user_id} - ) +# Returns: +# 数据存在状态 +# """ +# try: +# api_logger.info( +# f"检查用户情绪建议数据是否存在: {request.end_user_id}", +# extra={"end_user_id": request.end_user_id} +# ) - # 从数据库获取建议 - data = await emotion_service.get_cached_suggestions( - end_user_id=request.end_user_id, - db=db - ) +# # 从数据库获取建议 +# data = await emotion_service.get_cached_suggestions( +# end_user_id=request.end_user_id, +# db=db +# ) - if data is None: - api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") - return fail( - BizCode.NOT_FOUND, - "情绪建议数据不存在,请点击右上角刷新进行初始化", - {"exists": False} - ) +# if data is None: +# api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") +# return fail( +# BizCode.NOT_FOUND, +# "情绪建议数据不存在,请点击右上角刷新进行初始化", +# {"exists": False} +# ) - api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") - return success(data={"exists": True}, msg="情绪建议数据已存在") +# api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") +# return success(data={"exists": True}, msg="情绪建议数据已存在") - except Exception as e: - api_logger.error( - f"检查情绪建议数据失败: {str(e)}", - extra={"end_user_id": request.end_user_id}, - exc_info=True - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"检查情绪建议数据失败: {str(e)}" - ) +# except Exception as e: +# api_logger.error( +# f"检查情绪建议数据失败: {str(e)}", +# extra={"end_user_id": request.end_user_id}, +# exc_info=True +# ) +# raise HTTPException( +# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, +# detail=f"检查情绪建议数据失败: {str(e)}" +# ) @router.post("/suggestions", response_model=ApiResponse) @@ -383,4 +383,4 @@ async def generate_emotion_suggestions( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"生成个性化建议失败: {str(e)}" - ) + ) \ No newline at end of file diff --git a/redbear-mem-benchmark b/redbear-mem-benchmark index 4b0257bb..8494e824 160000 --- a/redbear-mem-benchmark +++ b/redbear-mem-benchmark @@ -1 +1 @@ -Subproject commit 4b0257bb4e7dc384b2aaf849b0bd6eae4b39835d +Subproject commit 8494e82498cb99c70ac67a64a544ff872432363a From bbb2c6c903460ae986e5113653f6ef68dcb1fba1 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 16:47:50 +0800 Subject: [PATCH 07/83] [changes] Modify the pop-up window for emotional suggestions at the backend --- api/app/controllers/emotion_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 8cfc5014..ea7b719f 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -295,8 +295,8 @@ async def get_emotion_suggestions( f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - return success( - data={"exists": False}, + return fail( + code=404, msg="情绪建议数据不存在,请点击右上角刷新进行初始化" ) From 31bee889d7a4eca95c42a4a2decdf9d462baf8fe Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 11:52:54 +0800 Subject: [PATCH 08/83] feat(web): model add is_vision/is_omni config --- web/src/i18n/en.ts | 8 +++- web/src/i18n/zh.ts | 6 +++ .../components/CustomModelModal.tsx | 44 +++++++++++++++---- .../ModelImplement/SubModelModal.tsx | 15 ++++--- .../components/ModelListDetail.tsx | 7 +-- .../components/ModelSquareDetail.tsx | 9 ++-- web/src/views/ModelManagement/types.ts | 13 ++++-- 7 files changed, 77 insertions(+), 25 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index d404dd6e..6bd3ea3f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -603,7 +603,13 @@ export const en = { ollama: "Ollama", xinference: "Xinference", gpustack: "Gpustack", - bedrock: "Bedrock" + bedrock: "Bedrock", + + is_vision: 'Vision Support', + is_omni: 'Omni Support', + vision: 'Vision', + audio: 'Audio', + video: 'Video', }, knowledgeBase: { home: 'Home', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index fc6bb822..43c3b8be 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1184,6 +1184,12 @@ export const zh = { xinference: "Xinference", gpustack: "Gpustack", bedrock: "Bedrock", + + is_vision: '支持视觉', + is_omni: '支持全模态', + vision: '视觉', + audio: '音频', + video: '视频', }, timezones: { 'Asia/Shanghai': '中国标准时间 (UTC+8)', diff --git a/web/src/views/ModelManagement/components/CustomModelModal.tsx b/web/src/views/ModelManagement/components/CustomModelModal.tsx index 112534a5..d47fc996 100644 --- a/web/src/views/ModelManagement/components/CustomModelModal.tsx +++ b/web/src/views/ModelManagement/components/CustomModelModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:49:28 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-28 17:24:05 + * @Last Modified time: 2026-03-04 11:31:43 */ /** * Custom Model Modal @@ -10,8 +10,8 @@ * Supports logo upload, type/provider selection, and tagging */ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Form, Input, App } from 'antd'; +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Form, Input, App, Checkbox } from 'antd'; import { useTranslation } from 'react-i18next'; import type { CustomModelForm, ModelListItem, CustomModelModalRef, CustomModelModalProps } from '../types'; @@ -35,6 +35,14 @@ const CustomModelModal = forwardRef( const [isEdit, setIsEdit] = useState(false); const [form] = Form.useForm(); const [loading, setLoading] = useState(false) + const modelType = Form.useWatch(['type'], form); + const isOmni = Form.useWatch(['is_omni'], form); + + useEffect(() => { + if (isOmni) { + form.setFieldsValue({ is_vision: true }) + } + }, [isOmni]) /** Close modal and reset state */ const handleClose = () => { @@ -49,9 +57,12 @@ const CustomModelModal = forwardRef( if (model) { setIsEdit(true); setModel(model); + const { capability, is_omni, ...rest} = model form.setFieldsValue({ - ...model, - logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined + ...rest, + logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined, + is_omni, + is_vision: capability?.includes('vision') || false, }); } else { setIsEdit(false); @@ -79,9 +90,14 @@ const CustomModelModal = forwardRef( form .validateFields() .then((values) => { - const { logo, ...rest } = values; + const { logo, type, is_vision, is_omni, ...rest } = values; const formData: CustomModelForm = { - ...rest + ...rest, + type, + } + if (!['embedding', 'rerank'].includes(type as string)) { + formData.capability = is_omni ? ["vision", "audio"] : is_vision ? ['vision'] : [] + formData.is_omni = is_omni } if (typeof logo === 'object' && logo?.response?.data.file_id) { @@ -108,7 +124,7 @@ const CustomModelModal = forwardRef( useImperativeHandle(ref, () => ({ handleOpen, })); - + console.log('modelType', modelType) return ( ( - ( > + + {!['embedding', 'rerank'].includes(modelType as string) && + <> + + {t('modelNew.is_omni')} + + + {t('modelNew.is_vision')} + + + } ); diff --git a/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx b/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx index e312b779..b2b44bf3 100644 --- a/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx +++ b/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:49:20 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:54:54 + * @Last Modified time: 2026-03-04 11:51:01 */ /** * Sub-Model Modal @@ -10,8 +10,8 @@ * Uses cascader for hierarchical selection */ -import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; -import { Form, Cascader, App, type CascaderProps } from 'antd'; +import { type ReactNode, forwardRef, useImperativeHandle, useState, useEffect } from 'react'; +import { Form, Cascader, App, type CascaderProps, Space } from 'antd'; import { useTranslation } from 'react-i18next'; import type { SubModelModalForm, SubModelModalRef, SubModelModalProps } from './types'; @@ -19,6 +19,7 @@ import RbModal from '@/components/RbModal' import CustomSelect from '@/components/CustomSelect' import { modelProviderUrl, getModelNewList } from '@/api/models' import type { ProviderModelItem } from '../../types' +import Tag from '@/components/Tag'; const { SHOW_CHILD } = Cascader; @@ -27,7 +28,7 @@ const { SHOW_CHILD } = Cascader; */ interface Option { value: string | number; - label: string; + label: string | ReactNode; children?: Option[]; [key: string]: any; } @@ -116,7 +117,11 @@ const SubModelModal = forwardRef(({ })) return { ...vo, - label: vo.name, + label: + {vo.name} + {t(`modelNew.${vo.type}`)} + {vo.capability?.filter(item => item !== 'video').map(vo => {t(`modelNew.${vo}`)})} + , value: vo.id, children: children } diff --git a/web/src/views/ModelManagement/components/ModelListDetail.tsx b/web/src/views/ModelManagement/components/ModelListDetail.tsx index aad7b887..5291d5c4 100644 --- a/web/src/views/ModelManagement/components/ModelListDetail.tsx +++ b/web/src/views/ModelManagement/components/ModelListDetail.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:49:45 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:49:45 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 11:50:47 */ /** * Model List Detail Drawer @@ -133,9 +133,10 @@ const ModelListDetail = forwardRef(({ + subTitle={ {t(`modelNew.${item.type}`)} {item.api_keys.length}{t('modelNew.apiKeyNum')} + {item.capability?.filter(item => item !=='video').map(vo => {t(`modelNew.${vo}`)})} } avatarUrl={getLogoUrl(item.logo)} avatar={ diff --git a/web/src/views/ModelManagement/components/ModelSquareDetail.tsx b/web/src/views/ModelManagement/components/ModelSquareDetail.tsx index 4fee5a7b..6826e9f5 100644 --- a/web/src/views/ModelManagement/components/ModelSquareDetail.tsx +++ b/web/src/views/ModelManagement/components/ModelSquareDetail.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:49:49 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:54:26 + * @Last Modified time: 2026-03-04 11:50:31 */ /** * Model Square Detail Drawer @@ -89,9 +89,10 @@ const ModelSquareDetail = forwardRef - {t(`modelNew.${item.type}`)} - {item.is_official && {t(`modelNew.official`)}} + subTitle={ + {t(`modelNew.${item.type}`)} + {item.is_official && {t(`modelNew.official`)}} + {item.capability?.filter(item => item !== 'video').map(vo => {t(`modelNew.${vo}`)})} } avatarUrl={getLogoUrl(item.logo)} avatar={ diff --git a/web/src/views/ModelManagement/types.ts b/web/src/views/ModelManagement/types.ts index e7e1f9ac..3233353b 100644 --- a/web/src/views/ModelManagement/types.ts +++ b/web/src/views/ModelManagement/types.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:50:18 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:50:18 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 11:39:20 */ /** * Type definitions for Model Management @@ -148,7 +148,9 @@ export interface ModelListItem { /** Update timestamp */ updated_at: number; /** Associated API keys */ - api_keys: ModelApiKey[] + api_keys: ModelApiKey[]; + capability?: string[]; + is_omni?: boolean; } /** @@ -261,6 +263,8 @@ export interface ModelPlazaItem { add_count: number; /** Whether user has added this model */ is_added: boolean; + capability?: string[]; + is_omni?: boolean; } /** @@ -291,6 +295,9 @@ export interface CustomModelForm { /** API base URL */ api_base: string; }> + is_vision?: boolean; + is_omni?: boolean; + capability?: string[]; } /** From 94836ed9af78e60c8417d7f174705822259b989c Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 12:28:55 +0800 Subject: [PATCH 09/83] [add] Set up scheduled tasks for existing and new users --- api/app/celery_app.py | 11 ++- api/app/core/config.py | 10 +- .../implicit_emotions_storage_repository.py | 48 ++++++++- api/app/tasks.py | 97 ++++++++++++++++++- 4 files changed, 155 insertions(+), 11 deletions(-) diff --git a/api/app/celery_app.py b/api/app/celery_app.py index 33fa1703..ba294651 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -4,6 +4,7 @@ from datetime import timedelta from urllib.parse import quote from celery import Celery +from celery.schedules import crontab from app.core.config import settings @@ -93,10 +94,12 @@ celery_app.autodiscover_tasks(['app']) # Celery Beat schedule for periodic tasks memory_increment_schedule = timedelta(hours=settings.MEMORY_INCREMENT_INTERVAL_HOURS) memory_cache_regeneration_schedule = timedelta(hours=settings.MEMORY_CACHE_REGENERATION_HOURS) -# 这个30秒的设计不合理 -workspace_reflection_schedule = timedelta(seconds=30) # 每30秒运行一次settings.REFLECTION_INTERVAL_TIME -forgetting_cycle_schedule = timedelta(hours=24) # 每24小时运行一次遗忘周期 -implicit_emotions_update_schedule = timedelta(hours=24) # 每24小时更新一次隐性记忆和情绪数据 +workspace_reflection_schedule = timedelta(seconds=settings.WORKSPACE_REFLECTION_INTERVAL_SECONDS) +forgetting_cycle_schedule = timedelta(hours=settings.FORGETTING_CYCLE_INTERVAL_HOURS) +implicit_emotions_update_schedule = crontab( + hour=settings.IMPLICIT_EMOTIONS_UPDATE_HOUR, + minute=settings.IMPLICIT_EMOTIONS_UPDATE_MINUTE, +) #构建定时任务配置 beat_schedule_config = { diff --git a/api/app/core/config.py b/api/app/core/config.py index 3a0c97b4..dc993e24 100644 --- a/api/app/core/config.py +++ b/api/app/core/config.py @@ -208,7 +208,15 @@ class Settings: # Memory Cache Regeneration Configuration MEMORY_CACHE_REGENERATION_HOURS: int = int(os.getenv("MEMORY_CACHE_REGENERATION_HOURS", "24")) - # Memory Module Configuration (internal) + # Periodic Task Schedule Configuration + # workspace_reflection: 每隔多少秒执行一次 + WORKSPACE_REFLECTION_INTERVAL_SECONDS: int = int(os.getenv("WORKSPACE_REFLECTION_INTERVAL_SECONDS", "30")) + # forgetting_cycle: 每隔多少小时执行一次 + FORGETTING_CYCLE_INTERVAL_HOURS: int = int(os.getenv("FORGETTING_CYCLE_INTERVAL_HOURS", "24")) + # implicit_emotions_update: 每天几点执行(小时,0-23) + IMPLICIT_EMOTIONS_UPDATE_HOUR: int = int(os.getenv("IMPLICIT_EMOTIONS_UPDATE_HOUR", "2")) + # implicit_emotions_update: 每天几分执行(分钟,0-59) + IMPLICIT_EMOTIONS_UPDATE_MINUTE: int = int(os.getenv("IMPLICIT_EMOTIONS_UPDATE_MINUTE", "0")) # Memory Module Configuration (internal) MEMORY_OUTPUT_DIR: str = os.getenv("MEMORY_OUTPUT_DIR", "logs/memory-output") MEMORY_CONFIG_DIR: str = os.getenv("MEMORY_CONFIG_DIR", "app/core/memory") diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py index 176012b7..1d11f89e 100644 --- a/api/app/repositories/implicit_emotions_storage_repository.py +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -5,12 +5,13 @@ Implicit Emotions Storage Repository 事务由调用方控制,仓储层只使用 flush/refresh """ import logging -from datetime import datetime +from datetime import datetime, date from typing import Optional, Generator from sqlalchemy.orm import Session -from sqlalchemy import select +from sqlalchemy import select, not_, exists from app.models.implicit_emotions_storage_model import ImplicitEmotionsStorage +from app.models.end_user_model import EndUser logger = logging.getLogger(__name__) @@ -110,6 +111,49 @@ class ImplicitEmotionsStorageRepository: logger.error(f"分批获取用户ID失败: offset={offset}, error={e}") break + def get_new_user_ids_today(self, batch_size: int = 100) -> Generator[str, None, None]: + """分批次获取当天新增的、尚未初始化隐性记忆和情绪建议数据的用户ID + + 查询逻辑:end_users 表中 created_at 为今天,且在 implicit_emotions_storage 中没有对应记录。 + 没有对应记录意味着隐性记忆画像和情绪建议均未初始化,需要对这批用户执行首次初始化。 + end_users.id(UUID)转为字符串后与 implicit_emotions_storage.end_user_id(String)对比。 + + Args: + batch_size: 每批次加载的数量,默认100 + + Yields: + 用户ID字符串 + """ + from sqlalchemy import cast, String as SAString + today_start = datetime.combine(date.today(), datetime.min.time()) + offset = 0 + while True: + try: + stmt = ( + select(EndUser.id) + .where( + EndUser.created_at >= today_start, + not_( + exists( + select(ImplicitEmotionsStorage.end_user_id).where( + ImplicitEmotionsStorage.end_user_id == cast(EndUser.id, SAString) + ) + ) + ) + ) + .order_by(EndUser.id) + .limit(batch_size) + .offset(offset) + ) + batch = self.db.execute(stmt).scalars().all() + if not batch: + break + yield from (str(uid) for uid in batch) + offset += batch_size + except Exception as e: + logger.error(f"分批获取当天新增用户ID失败: offset={offset}, error={e}") + break + def delete_by_end_user_id(self, end_user_id: str) -> bool: """删除用户的存储记录(事务由调用方提交)""" storage = self.get_by_end_user_id(end_user_id) diff --git a/api/app/tasks.py b/api/app/tasks.py index 1675f25d..f30bbb81 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -2017,7 +2017,7 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: # 更新情绪建议 try: - emotion_service = EmotionAnalyticsService(db=db, end_user_id=end_user_id) + emotion_service = EmotionAnalyticsService() suggestions_data = await emotion_service.generate_emotion_suggestions( end_user_id=end_user_id, db=db, @@ -2076,22 +2076,109 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: user_results.append(error_info) logger.error(f"处理用户 {end_user_id} 时出错: {str(e)}") + # ---- 处理增量用户(当天新增、尚未初始化的用户)---- + new_users_initialized = 0 + new_users_failed = 0 + logger.info("开始处理当天新增的增量用户初始化") + + for end_user_id in repo.get_new_user_ids_today(batch_size=100): + logger.info(f"开始初始化新用户: {end_user_id}") + user_start_time = time.time() + implicit_success = False + emotion_success = False + errors = [] + + try: + try: + implicit_service = ImplicitMemoryService(db=db, end_user_id=end_user_id) + profile_data = await implicit_service.generate_complete_profile(user_id=end_user_id) + await implicit_service.save_profile_cache( + end_user_id=end_user_id, + profile_data=profile_data, + db=db + ) + implicit_success = True + logger.info(f"成功初始化新用户 {end_user_id} 的隐性记忆画像") + except Exception as e: + error_msg = f"隐性记忆初始化失败: {str(e)}" + errors.append(error_msg) + logger.error(f"新用户 {end_user_id} {error_msg}") + + try: + emotion_service = EmotionAnalyticsService() + suggestions_data = await emotion_service.generate_emotion_suggestions( + end_user_id=end_user_id, + db=db, + language="zh" + ) + await emotion_service.save_suggestions_cache( + end_user_id=end_user_id, + suggestions_data=suggestions_data, + db=db + ) + emotion_success = True + logger.info(f"成功初始化新用户 {end_user_id} 的情绪建议") + except Exception as e: + error_msg = f"情绪建议初始化失败: {str(e)}" + errors.append(error_msg) + logger.error(f"新用户 {end_user_id} {error_msg}") + + if implicit_success or emotion_success: + new_users_initialized += 1 + else: + new_users_failed += 1 + + user_elapsed = time.time() - user_start_time + user_results.append({ + "end_user_id": end_user_id, + "type": "init", + "implicit_success": implicit_success, + "emotion_success": emotion_success, + "errors": errors, + "elapsed_time": user_elapsed + }) + + except Exception as e: + new_users_failed += 1 + user_elapsed = time.time() - user_start_time + user_results.append({ + "end_user_id": end_user_id, + "type": "init", + "implicit_success": False, + "emotion_success": False, + "errors": [str(e)], + "elapsed_time": user_elapsed + }) + logger.error(f"初始化新用户 {end_user_id} 时出错: {str(e)}") + + logger.info( + f"增量用户初始化完成: 成功={new_users_initialized}, 失败={new_users_failed}" + ) + # ---- 增量用户处理结束 ---- + # 记录总体统计信息 logger.info( f"隐性记忆和情绪数据更新定时任务完成: " - f"总用户数={total_users}, " + f"存量用户总数={total_users}, " f"隐性记忆成功={successful_implicit}, " f"情绪建议成功={successful_emotion}, " - f"失败={failed}" + f"存量失败={failed}, " + f"增量初始化成功={new_users_initialized}, " + f"增量初始化失败={new_users_failed}" ) return { "status": "SUCCESS", - "message": f"成功处理 {total_users} 个用户,隐性记忆 {successful_implicit} 个成功,情绪建议 {successful_emotion} 个成功", + "message": ( + f"存量用户 {total_users} 个,隐性记忆 {successful_implicit} 个成功,情绪建议 {successful_emotion} 个成功;" + f"增量新用户初始化 {new_users_initialized} 个成功,{new_users_failed} 个失败" + ), "total_users": total_users, "successful_implicit": successful_implicit, "successful_emotion": successful_emotion, "failed": failed, + "new_users_initialized": new_users_initialized, + "new_users_failed": new_users_failed, "user_results": user_results[:50] # 只保留前50个用户的详细结果 } @@ -2104,6 +2191,8 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: "successful_implicit": successful_implicit, "successful_emotion": successful_emotion, "failed": failed, + "new_users_initialized": 0, + "new_users_failed": 0, "user_results": user_results[:50] } From 78fd1895104c9a9082fe121d24d9c69baf5f404b Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 27 Feb 2026 10:13:49 +0800 Subject: [PATCH 10/83] fix(web): release bugfix --- .../components/ChangeEmailModal.tsx | 6 +- .../views/Workflow/components/Chat/Chat.tsx | 812 +----------------- 2 files changed, 5 insertions(+), 813 deletions(-) diff --git a/web/src/views/UserManagement/components/ChangeEmailModal.tsx b/web/src/views/UserManagement/components/ChangeEmailModal.tsx index fbf93480..64791519 100644 --- a/web/src/views/UserManagement/components/ChangeEmailModal.tsx +++ b/web/src/views/UserManagement/components/ChangeEmailModal.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-25 11:45:07 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-25 11:45:07 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-27 09:59:41 */ /** * ChangeEmailModal Component @@ -114,7 +114,7 @@ const ChangeEmailModal = forwardRef( sendEmailCode({ email: values.new_email }) .then(() => { message.success(t('user.sendSuccess')) - setCountdown(300) + setCountdown(60) const timer = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 51b1be38..895ade24 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-24 17:55:08 + * @Last Modified time: 2026-02-27 09:58:30 */ /** * Workflow Chat Component @@ -50,815 +50,7 @@ const Chat = forwardRef(({ appId // State management const [open, setOpen] = useState(false) // Drawer visibility const [loading, setLoading] = useState(false) // Send button loading state - const [chatList, setChatList] = useState([ - { - "role": "assistant", - "content": "经过多次打磨,最终作品如下:\n《咏一·三题》 \n孤光未凿太初溟, \n一粟吞天万籁宁。 \n影堕千峰青未染, \n心空四象白犹灵。 \n非从烛焰求明性, \n但向尘劳见本形。 \n忽有松风穿石罅, \n泠然吹落满山星。 \n\n注:本诗严守平水韵九青部(溟、宁、灵、形、星),其中“星”属下平声九青部异读字(《广韵》息盈切,与“灵”“宁”同部),古诗常用以协律,如王维“清溪流过碧山头,空水澄鲜一色秋。隔断红尘三十里,白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨:首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典,反写太初本明未分之境;次句“一粟吞天”,以微纳巨,承“一芥”而力愈雄浑;颔联“青未染”“白犹灵”,双色映照,暗喻性体离垢绝染而朗然常照;颈联直破二边——不假烛焰(破外求)、不避尘劳(破厌离),显《坛经》“佛法在世间,不离世间觉”之旨;结句松风裂石、星落满山,是“一”之活泼妙用:寂而常照,照而恒寂,恰如《道德经》“天得一以清,地得一以宁”之诗性证成。 \nLLM1结果:\n《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。 ", - "created_at": 1771925594511, - "subContent": [ - { - "id": "start_1767617465337_0djnmpk2y", - "node_id": "start_1767617465337_0djnmpk2y", - "node_name": "开始(Start)", - "icon": "/src/assets/images/workflow/start.png", - "content": { - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "topic": "", - "number": 0, - "Boolean": false - } - }, - "status": "completed", - "elapsed_time": 0 - }, - { - "id": "llm_1767617499720_zvqwjpw3b", - "node_id": "llm_1767617499720_zvqwjpw3b", - "node_name": "大语言模型 (LLM)-初始创作", - "icon": "/src/assets/images/workflow/llm.png", - "content": { - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据1 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。" - }, - "status": "completed", - "elapsed_time": 4.518743515014648 - }, - { - "id": "loop_1767617552451_hq3j342ha", - "node_id": "loop_1767617552451_hq3j342ha", - "node_name": "循环 (Loop)", - "icon": "/src/assets/images/workflow/loop.png", - "content": { - "input": { - "config": { - "max_loop": 10, - "condition": { - "expressions": [ - { - "left": "{{loop_1767617552451_hq3j342ha.round}}", - "right": 3, - "operator": "eq", - "input_type": "Constant" - } - ], - "logical_operator": "and" - }, - "cycle_vars": [ - { - "name": "poem_content", - "type": "string", - "value": "{{llm_1767617499720_zvqwjpw3b.output}}", - "input_type": "variable" - }, - { - "name": "round", - "type": "number", - "value": "0", - "input_type": "constant" - } - ] - } - }, - "output": { - "poem_content": "《咏一·三题》 \n孤光未凿太初溟, \n一粟吞天万籁宁。 \n影堕千峰青未染, \n心空四象白犹灵。 \n非从烛焰求明性, \n但向尘劳见本形。 \n忽有松风穿石罅, \n泠然吹落满山星。 \n\n注:本诗严守平水韵九青部(溟、宁、灵、形、星),其中“星”属下平声九青部异读字(《广韵》息盈切,与“灵”“宁”同部),古诗常用以协律,如王维“清溪流过碧山头,空水澄鲜一色秋。隔断红尘三十里,白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨:首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典,反写太初本明未分之境;次句“一粟吞天”,以微纳巨,承“一芥”而力愈雄浑;颔联“青未染”“白犹灵”,双色映照,暗喻性体离垢绝染而朗然常照;颈联直破二边——不假烛焰(破外求)、不避尘劳(破厌离),显《坛经》“佛法在世间,不离世间觉”之旨;结句松风裂石、星落满山,是“一”之活泼妙用:寂而常照,照而恒寂,恰如《道德经》“天得一以清,地得一以宁”之诗性证成。", - "round": 3, - "__child_state": [ - { - "messages": [], - "cycle_nodes": [ - "loop_1767617552451_hq3j342ha" - ], - "looping": 1, - "node_outputs": { - "start_1767617465337_0djnmpk2y": { - "node_id": "start_1767617465337_0djnmpk2y", - "node_type": "start", - "node_name": "开始(Start)", - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "topic": "", - "number": 0, - "Boolean": false - }, - "elapsed_time": 0, - "token_usage": null, - "error": null - }, - "llm_1767617499720_zvqwjpw3b": { - "node_id": "llm_1767617499720_zvqwjpw3b", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-初始创作", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据1 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "elapsed_time": 4.518743515014648, - "token_usage": { - "prompt_tokens": 25, - "completion_tokens": 165, - "total_tokens": 190 - }, - "error": null - }, - "loop_1767617552451_hq3j342ha": { - "poem_content": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "round": 0 - }, - "21046fb8-1f33-45f7-aeda-2c196471f119": { - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_type": "cycle-start", - "node_name": null, - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd" - }, - "elapsed_time": 0.0005278587341308594, - "token_usage": null, - "error": null - }, - "llm_1767617560401_bsx1vhi25": { - "node_id": "llm_1767617560401_bsx1vhi25", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-润色器", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。", - "elapsed_time": 6.8497374057769775, - "token_usage": { - "prompt_tokens": 188, - "completion_tokens": 262, - "total_tokens": 450 - }, - "error": null - }, - "assigner_1768285417545_qsoqleflh": { - "node_id": "assigner_1768285417545_qsoqleflh", - "node_type": "assigner", - "node_name": "变量赋值", - "status": "completed", - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null, - "elapsed_time": 0.0003705024719238281, - "token_usage": null, - "error": null - } - }, - "execution_id": "exec_11a80fb1cde148cb", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "error": null, - "error_node": null, - "activate": { - "llm_1767617560401_bsx1vhi25": true, - "loop_1767617552451_hq3j342ha": true, - "start_1767617465337_0djnmpk2y": true, - "21046fb8-1f33-45f7-aeda-2c196471f119": true, - "llm_1767617499720_zvqwjpw3b": true, - "assigner_1768285417545_qsoqleflh": true - } - }, - { - "messages": [], - "cycle_nodes": [ - "loop_1767617552451_hq3j342ha" - ], - "looping": 1, - "node_outputs": { - "start_1767617465337_0djnmpk2y": { - "node_id": "start_1767617465337_0djnmpk2y", - "node_type": "start", - "node_name": "开始(Start)", - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "topic": "", - "number": 0, - "Boolean": false - }, - "elapsed_time": 0, - "token_usage": null, - "error": null - }, - "llm_1767617499720_zvqwjpw3b": { - "node_id": "llm_1767617499720_zvqwjpw3b", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-初始创作", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据1 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "elapsed_time": 4.518743515014648, - "token_usage": { - "prompt_tokens": 25, - "completion_tokens": 165, - "total_tokens": 190 - }, - "error": null - }, - "loop_1767617552451_hq3j342ha": { - "poem_content": "《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。", - "round": 1 - }, - "21046fb8-1f33-45f7-aeda-2c196471f119": { - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_type": "cycle-start", - "node_name": null, - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd" - }, - "elapsed_time": 0, - "token_usage": null, - "error": null - }, - "llm_1767617560401_bsx1vhi25": { - "node_id": "llm_1767617560401_bsx1vhi25", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-润色器", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。", - "elapsed_time": 7.1851232051849365, - "token_usage": { - "prompt_tokens": 285, - "completion_tokens": 281, - "total_tokens": 566 - }, - "error": null - }, - "assigner_1768285417545_qsoqleflh": { - "node_id": "assigner_1768285417545_qsoqleflh", - "node_type": "assigner", - "node_name": "变量赋值", - "status": "completed", - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null, - "elapsed_time": 0, - "token_usage": null, - "error": null - } - }, - "execution_id": "exec_11a80fb1cde148cb", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "error": null, - "error_node": null, - "activate": { - "llm_1767617560401_bsx1vhi25": true, - "start_1767617465337_0djnmpk2y": true, - "loop_1767617552451_hq3j342ha": true, - "21046fb8-1f33-45f7-aeda-2c196471f119": true, - "llm_1767617499720_zvqwjpw3b": true, - "assigner_1768285417545_qsoqleflh": true - } - }, - { - "messages": [], - "cycle_nodes": [ - "loop_1767617552451_hq3j342ha" - ], - "looping": 1, - "node_outputs": { - "start_1767617465337_0djnmpk2y": { - "node_id": "start_1767617465337_0djnmpk2y", - "node_type": "start", - "node_name": "开始(Start)", - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "topic": "", - "number": 0, - "Boolean": false - }, - "elapsed_time": 0, - "token_usage": null, - "error": null - }, - "llm_1767617499720_zvqwjpw3b": { - "node_id": "llm_1767617499720_zvqwjpw3b", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-初始创作", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据1 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "elapsed_time": 4.518743515014648, - "token_usage": { - "prompt_tokens": 25, - "completion_tokens": 165, - "total_tokens": 190 - }, - "error": null - }, - "loop_1767617552451_hq3j342ha": { - "poem_content": "《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。", - "round": 2 - }, - "21046fb8-1f33-45f7-aeda-2c196471f119": { - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_type": "cycle-start", - "node_name": null, - "status": "completed", - "input": { - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "message": "1", - "conversation_vars": {} - }, - "output": { - "message": "1", - "execution_id": "exec_11a80fb1cde148cb", - "conversation_id": "37ee003e-cc53-47e7-930f-a436a1252dd1", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd" - }, - "elapsed_time": 0, - "token_usage": null, - "error": null - }, - "llm_1767617560401_bsx1vhi25": { - "node_id": "llm_1767617560401_bsx1vhi25", - "node_type": "llm", - "node_name": "大语言模型 (LLM)-润色器", - "status": "completed", - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·三题》 \n孤光未凿太初溟, \n一粟吞天万籁宁。 \n影堕千峰青未染, \n心空四象白犹灵。 \n非从烛焰求明性, \n但向尘劳见本形。 \n忽有松风穿石罅, \n泠然吹落满山星。 \n\n注:本诗严守平水韵九青部(溟、宁、灵、形、星),其中“星”属下平声九青部异读字(《广韵》息盈切,与“灵”“宁”同部),古诗常用以协律,如王维“清溪流过碧山头,空水澄鲜一色秋。隔断红尘三十里,白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨:首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典,反写太初本明未分之境;次句“一粟吞天”,以微纳巨,承“一芥”而力愈雄浑;颔联“青未染”“白犹灵”,双色映照,暗喻性体离垢绝染而朗然常照;颈联直破二边——不假烛焰(破外求)、不避尘劳(破厌离),显《坛经》“佛法在世间,不离世间觉”之旨;结句松风裂石、星落满山,是“一”之活泼妙用:寂而常照,照而恒寂,恰如《道德经》“天得一以清,地得一以宁”之诗性证成。", - "elapsed_time": 9.531717538833618, - "token_usage": { - "prompt_tokens": 304, - "completion_tokens": 390, - "total_tokens": 694 - }, - "error": null - }, - "assigner_1768285417545_qsoqleflh": { - "node_id": "assigner_1768285417545_qsoqleflh", - "node_type": "assigner", - "node_name": "变量赋值", - "status": "completed", - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null, - "elapsed_time": 0, - "token_usage": null, - "error": null - } - }, - "execution_id": "exec_11a80fb1cde148cb", - "workspace_id": "d17cd62d-a725-4fc0-813b-1093f2dfdee4", - "user_id": "ab27a27f-072b-47e9-8bbb-1f19322debcd", - "error": null, - "error_node": null, - "activate": { - "llm_1767617560401_bsx1vhi25": true, - "start_1767617465337_0djnmpk2y": true, - "loop_1767617552451_hq3j342ha": true, - "21046fb8-1f33-45f7-aeda-2c196471f119": true, - "llm_1767617499720_zvqwjpw3b": true, - "assigner_1768285417545_qsoqleflh": true - } - } - ] - } - }, - "subContent": [ - { - "cycle_idx": 0, - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_name": null, - "icon": "/src/assets/images/workflow/loop.png", - "content": { - "cycle_idx": 0, - "input": { - "poem_content": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "round": 0 - }, - "output": { - "poem_content": "《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。", - "round": 0 - } - }, - "status": "completed", - "elapsed_time": 0.0005278587341308594 - }, - { - "cycle_idx": 0, - "node_id": "llm_1767617560401_bsx1vhi25", - "node_name": "大语言模型 (LLM)-润色器", - "icon": "/src/assets/images/workflow/llm.png", - "content": { - "cycle_idx": 0, - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。" - }, - "status": "completed", - "elapsed_time": 6.8497374057769775 - }, - { - "cycle_idx": 0, - "node_id": "assigner_1768285417545_qsoqleflh", - "node_name": "变量赋值", - "icon": "/src/assets/images/workflow/assigner.png", - "content": { - "cycle_idx": 0, - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null - }, - "status": "completed", - "elapsed_time": 0.0003705024719238281 - }, - { - "cycle_idx": 1, - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_name": null, - "icon": "/src/assets/images/workflow/loop.png", - "content": { - "cycle_idx": 1, - "input": { - "poem_content": "《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。", - "round": 1 - }, - "output": { - "poem_content": "《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。", - "round": 1 - } - }, - "status": "completed", - "elapsed_time": 0 - }, - { - "cycle_idx": 1, - "node_id": "llm_1767617560401_bsx1vhi25", - "node_name": "大语言模型 (LLM)-润色器", - "icon": "/src/assets/images/workflow/llm.png", - "content": { - "cycle_idx": 1, - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一·次韵》 \n千峰削玉立空青, \n一羽浮天亦自宁。 \n万籁收声归太始, \n孤光未堕即长明。 \n\n注:本诗承原作“以一为魂”之旨,严守平水韵九青部(青、宁、明),平仄谐律。首句“千峰削玉”反衬“一羽浮天”,以极繁托极简;次句“一羽”既承“一芥”之微,更取《庄子》“鹏徙南冥”之逸气,言至微者亦可持守本然之宁。三句“万籁收声”暗应原作“千山雪落只无声”,而升华为宇宙初开的“太始”静界;结句“孤光未堕即长明”,化用《淮南子》“日月不为明而明”与禅宗“一念不生即佛”,昭示“一”非寂灭之空,乃不假外求、本自圆成的永恒觉性——此即《道德经》“天得一以清”的诗性证悟。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。" - }, - "status": "completed", - "elapsed_time": 7.1851232051849365 - }, - { - "cycle_idx": 1, - "node_id": "assigner_1768285417545_qsoqleflh", - "node_name": "变量赋值", - "icon": "/src/assets/images/workflow/assigner.png", - "content": { - "cycle_idx": 1, - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null - }, - "status": "completed", - "elapsed_time": 0 - }, - { - "cycle_idx": 2, - "node_id": "21046fb8-1f33-45f7-aeda-2c196471f119", - "node_name": null, - "icon": "/src/assets/images/workflow/loop.png", - "content": { - "cycle_idx": 2, - "input": { - "poem_content": "《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。", - "round": 2 - }, - "output": { - "poem_content": "《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。", - "round": 2 - } - }, - "status": "completed", - "elapsed_time": 0 - }, - { - "cycle_idx": 2, - "node_id": "llm_1767617560401_bsx1vhi25", - "node_name": "大语言模型 (LLM)-润色器", - "icon": "/src/assets/images/workflow/llm.png", - "content": { - "cycle_idx": 2, - "input": { - "prompt": null, - "messages": [ - { - "role": "system", - "content": "请根据《咏一·再题》 \n一芥浮空万籁停, \n千峰影落太初青。 \n光非燃烛恒明在, \n心不沾尘即性灵。 \n\n注:本诗续写“以一为魂”之旨,严守平水韵九青部(停、青、灵),平仄精严。首句“一芥”承原作微渺意象,而“万籁停”较“收声”更显寂然自定之境;次句倒装“千峰影落”,使苍茫山势如墨痕沉入宇宙初青,暗契《淮南子》“虚霩生宇宙,宇宙生气”之太始气象。三句翻出新境:“光非燃烛”,破除对光明之形器执取,直指《楞严经》“性觉妙明,本觉明妙”之不假缘起的本明;结句“心不沾尘即性灵”,化用六祖“本来无一物”与程颢“天地之大德曰生”,言“一”非枯寂之数,乃活泼泼的性灵朗现——此即《道德经》“昔之得一者,天清地宁”的诗性澄明。 为主题写一首七字诗。" - } - ], - "config": { - "model_id": "2699984d-23be-4817-b81c-c38682a08306", - "temperature": 0.7, - "max_tokens": 2000 - } - }, - "output": "《咏一·三题》 \n孤光未凿太初溟, \n一粟吞天万籁宁。 \n影堕千峰青未染, \n心空四象白犹灵。 \n非从烛焰求明性, \n但向尘劳见本形。 \n忽有松风穿石罅, \n泠然吹落满山星。 \n\n注:本诗严守平水韵九青部(溟、宁、灵、形、星),其中“星”属下平声九青部异读字(《广韵》息盈切,与“灵”“宁”同部),古诗常用以协律,如王维“清溪流过碧山头,空水澄鲜一色秋。隔断红尘三十里,白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨:首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典,反写太初本明未分之境;次句“一粟吞天”,以微纳巨,承“一芥”而力愈雄浑;颔联“青未染”“白犹灵”,双色映照,暗喻性体离垢绝染而朗然常照;颈联直破二边——不假烛焰(破外求)、不避尘劳(破厌离),显《坛经》“佛法在世间,不离世间觉”之旨;结句松风裂石、星落满山,是“一”之活泼妙用:寂而常照,照而恒寂,恰如《道德经》“天得一以清,地得一以宁”之诗性证成。" - }, - "status": "completed", - "elapsed_time": 9.531717538833618 - }, - { - "cycle_idx": 2, - "node_id": "assigner_1768285417545_qsoqleflh", - "node_name": "变量赋值", - "icon": "/src/assets/images/workflow/assigner.png", - "content": { - "cycle_idx": 2, - "input": { - "config": { - "assignments": [ - { - "value": "{{llm_1767617560401_bsx1vhi25.output}}", - "operation": "cover", - "variable_selector": "{{loop_1767617552451_hq3j342ha.poem_content}}" - }, - { - "value": 1, - "operation": "add", - "variable_selector": "{{loop_1767617552451_hq3j342ha.round}}" - } - ] - } - }, - "output": null - }, - "status": "completed", - "elapsed_time": 0 - } - ], - "status": "completed", - "elapsed_time": 23.57662582397461 - }, - { - "id": "end_1767619139811_ko97mb12l", - "node_id": "end_1767619139811_ko97mb12l", - "node_name": "结束(End)", - "icon": "/src/assets/images/workflow/end.png", - "content": { - "input": { - "config": { - "output": "经过多次打磨,最终作品如下:\n{{loop_1767617552451_hq3j342ha.poem_content}} \nLLM1结果:\n{{llm_1767617499720_zvqwjpw3b.output}} " - } - }, - "output": "经过多次打磨,最终作品如下:\n《咏一·三题》 \n孤光未凿太初溟, \n一粟吞天万籁宁。 \n影堕千峰青未染, \n心空四象白犹灵。 \n非从烛焰求明性, \n但向尘劳见本形。 \n忽有松风穿石罅, \n泠然吹落满山星。 \n\n注:本诗严守平水韵九青部(溟、宁、灵、形、星),其中“星”属下平声九青部异读字(《广韵》息盈切,与“灵”“宁”同部),古诗常用以协律,如王维“清溪流过碧山头,空水澄鲜一色秋。隔断红尘三十里,白云红叶两悠悠”中“悠”亦借韵通协。全诗紧扣“以一为魂”之旨:首句“孤光未凿”化《庄子·应帝王》“浑沌凿七窍而死”典,反写太初本明未分之境;次句“一粟吞天”,以微纳巨,承“一芥”而力愈雄浑;颔联“青未染”“白犹灵”,双色映照,暗喻性体离垢绝染而朗然常照;颈联直破二边——不假烛焰(破外求)、不避尘劳(破厌离),显《坛经》“佛法在世间,不离世间觉”之旨;结句松风裂石、星落满山,是“一”之活泼妙用:寂而常照,照而恒寂,恰如《道德经》“天得一以清,地得一以宁”之诗性证成。 \nLLM1结果:\n《咏一》 \n孤峰独峙破苍冥, \n一芥微身立太清。 \n万古乾坤凝此数, \n千山雪落只无声。 \n\n注:本诗以“一”为魂,通过“孤峰”“一芥”“此数”层层递进,赋予数字哲思——既写天地间唯一性之壮美(孤峰破冥),又寓渺小个体与永恒宇宙的辩证(芥子纳太清)。末句“千山雪落只无声”,以大静写大一,雪覆千山而声息俱寂,暗合《道德经》“天得一以清”之境。平仄依平水韵,押九青部(冥、清、声)。 " - }, - "status": "completed", - "elapsed_time": 0.0005218982696533203 - } - ], - "status": "completed" - } - ]) // Chat message history + const [chatList, setChatList] = useState([]) // Chat message history const [variables, setVariables] = useState([]) // Workflow input variables const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state const [conversationId, setConversationId] = useState(null) // Current conversation ID From 34310bfabe1559834223198b0f8452c1cb69147b Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 26 Feb 2026 16:22:45 +0800 Subject: [PATCH 11/83] fix(version): fix version information --- api/app/version_info.json | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/api/app/version_info.json b/api/app/version_info.json index 991369d7..aea03dcd 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -1,4 +1,38 @@ { + "v0.2.4": { + "introduction": { + "codeName": "智远", + "releaseDate": "2026-2-11", + "upgradePosition": "🐻 生产级稳健性升级版本,智慧致远,从容应对复杂场景", + "coreUpgrades": [ + "1. Skills 技能框架 🛠️
* Skills 支持:引入全新的Skills技能系统,支持可扩展的能力模块,可在Agent和工作流中动态加载与编排", + "2. 多模态与交互 💬
* 文件多模态支持:全面支持消息输入、LLM处理和输出渲染中的多模态文件处理,实现更丰富的媒体感知对话
* 语音交互:语音交互功能正在积极开发中,为免提对话体验奠定基础(开发中)", + "3. 知识库集成 📚
* 飞书知识库:无缝对接飞书文档库,支持企业知识检索
* 语雀知识库:原生连接语雀文档平台,扩展对国内企业工具生态的覆盖
* Web站点知识库:通用Web站点抓取与索引,支持从公开网页内容构建知识库
* 视觉模型选择优化:知识库视觉模型配置现已支持LLM和Chat两种模型类型,移除了此前仅限Chat类型的限制", + "4. 记忆智能 🧠
* 本体工程(二期):基于本体工程的高级记忆场景分类与萃取,实现结构化、领域感知的记忆组织,提升分类准确性
* 默认模型配置:情绪分析、反思和记忆萃取模块现默认使用空间级模型,确保开箱即用的一致性行为
* 智能模型回退:当已配置的情绪或反思模型为空或不可用时,系统自动回退至空间默认模型,避免静默失败
* 记忆模型回退兜底:当记忆中配置的模型为空或不可用时,系统优雅降级至空间默认模型", + "5. 性能与扩展 ⚡
* 模型并发(model_api_keys):支持并发模型API Key管理,实现并行模型调用,提升高负载场景下的吞吐能力", + "6. 稳健性与缺陷修复 🔧
* 记忆配置版本固定:修复用户记忆配置未跟随应用版本发布固定的问题,消除跨部署的行为不一致
* 空间默认记忆保护:空间级默认记忆配置现不可删除;用户级配置仍可删除
* Agent与工作流配置兜底:解决Agent和工作流节点中记忆配置可能为空、或已选择但未配置的边界情况——全面的回退处理现可防止运行时错误
* 隐形记忆字段重命名:将隐形记忆接口JSON响应中的user_id修正为end_user_id,与规范数据模型对齐
* 记忆配置ID迁移:将Agent和工作流记忆配置中的memory_content重命名为memory_config_id,保持API一致性
* Worker-Memory告警解决:解决worker-memory服务中的告警级别问题,提升运维监控清晰度
* 双语接口修复:修复记忆相关API接口的中英文不一致问题
* 新用户记忆配置自动回填:新创建的EndUser若memory_config_id为None,系统自动从最新Release获取memory_config_id并回填
* 存量用户记忆配置自动回填:已有EndUser若memory_config_id为None,系统同样从最新Release获取并回填,确保向后兼容,无需手动迁移", + "
", + "Memory Bear v0.2.4 向生产级稳健性迈进,Skills框架与多模态支持开启认知平台新篇章。", + "记忆熊,智慧致远,从容应对真实世界的多样性。🐻✨" + ] + }, + "introduction_en": { + "codeName": "ZhiYuan", + "releaseDate": "2026-2-11", + "upgradePosition": "🐻 Production-grade resilience release — Wisdom Reaching Far, gracefully handling complex scenarios", + "coreUpgrades": [ + "1. Skills Framework 🛠️
* Skills Support: Introduced a new Skills system, enabling extensible capability modules that can be dynamically loaded and orchestrated within agents and workflows", + "2. Multimodal & Interaction 💬
* File Multimodal Support: Full multimodal file handling across message input, LLM processing, and output rendering — supporting richer, media-aware conversations
* Voice Interaction: Voice-based interaction capabilities are under active development, laying the groundwork for hands-free conversational experiences (In Progress)", + "3. Knowledge Base Integration 📚
* Feishu Knowledge Base: Seamless integration with Feishu (Lark) document repositories for enterprise knowledge retrieval
* Yuque Knowledge Base: Native connector for Yuque documentation platforms, expanding coverage of Chinese enterprise tooling
* Web Site Knowledge Base: General-purpose web site crawling and indexing for knowledge base construction from public web content
* Visual Model Selection: Knowledge base visual model configuration now supports both LLM and Chat model types, removing the previous restriction to Chat-only selection", + "4. Memory Intelligence 🧠
* Ontology Engineering (Phase 2): Advanced memory scene classification and extraction powered by ontology engineering — enabling structured, domain-aware memory organization with improved categorization accuracy
* Default Model Configuration: Emotion analysis, reflection, and memory extraction modules now default to the space-level model, ensuring consistent behavior out of the box
* Intelligent Model Fallback: If configured emotion or reflection models are empty or unavailable, the system automatically falls back to the space default model — preventing silent failures
* Memory Config Fallback for Models: When any memory-configured model is empty or unavailable, the system gracefully degrades to the space default model", + "5. Performance & Scalability ⚡
* Model Concurrency (model_api_keys): Support for concurrent model API key management, enabling parallel model invocations and improved throughput for high-load scenarios", + "6. Robustness & Bug Fixes 🔧
* Memory Config Version Pinning: Fixed an issue where user memory configurations were not pinned to application release versions, causing inconsistent behavior across deployments
* Space Default Memory Protection: Space-level default memory configurations are now protected from deletion; user-level configurations remain deletable
* Agent & Workflow Config Fallback: Resolved edge cases in Agent and Workflow nodes where memory config could be empty or selected but unconfigured — comprehensive fallback handling now prevents runtime errors
* Implicit Memory Field Rename: Corrected user_id to end_user_id in JSON responses from implicit memory interfaces, aligning with the canonical data model
* Memory Config ID Migration: Renamed memory_content to memory_config_id in Agent and Workflow memory configurations for API consistency
* Worker-Memory Alerts: Resolved warning-level alerts in the worker-memory service, improving operational monitoring clarity
* Bilingual Interface Fixes: Fixed Chinese/English language inconsistencies across memory-related API interfaces
* EndUser Memory Config Auto-Backfill (New Users): When a newly created EndUser has memory_config_id as None, the system automatically fetches the latest release's memory_config_id and backfills it
* EndUser Memory Config Auto-Backfill (Existing Users): For existing EndUsers with memory_config_id as None, the system similarly retrieves and backfills from the latest release — ensuring backward compatibility without manual migration", + "
", + "Memory Bear v0.2.4 advances toward production-grade resilience, with the Skills framework and multimodal support opening a new chapter for the cognitive platform.", + "MemoryBear — Wisdom Reaching Far, gracefully handling real-world variability. 🐻✨" + ] + } + }, "v0.2.3": { "introduction": { "codeName": "归墟", From a39ba564faeb11a68520f01442ff038f6e4a4d02 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 26 Feb 2026 17:09:50 +0800 Subject: [PATCH 12/83] fix(file): File uploads can be made without workspace. --- api/app/models/file_metadata_model.py | 2 +- api/app/services/file_storage_service.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/app/models/file_metadata_model.py b/api/app/models/file_metadata_model.py index baf9bd97..28e87367 100644 --- a/api/app/models/file_metadata_model.py +++ b/api/app/models/file_metadata_model.py @@ -35,7 +35,7 @@ class FileMetadata(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True, comment="Tenant ID") - workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True, comment="Workspace ID") + workspace_id = Column(UUID(as_uuid=True), nullable=True, index=True, comment="Workspace ID") file_key = Column(String(512), nullable=False, unique=True, index=True, comment="Storage file key") file_name = Column(String(255), nullable=False, comment="Original file name") file_ext = Column(String(32), nullable=False, comment="File extension") diff --git a/api/app/services/file_storage_service.py b/api/app/services/file_storage_service.py index 672e1cff..bb9f1894 100644 --- a/api/app/services/file_storage_service.py +++ b/api/app/services/file_storage_service.py @@ -26,7 +26,7 @@ logger = get_business_logger() def generate_file_key( tenant_id: uuid.UUID, - workspace_id: uuid.UUID, + workspace_id: uuid.UUID | None, file_id: uuid.UUID, file_ext: str, ) -> str: @@ -56,8 +56,9 @@ def generate_file_key( # Ensure file_ext starts with a dot if file_ext and not file_ext.startswith('.'): file_ext = f'.{file_ext}' - - return f"{tenant_id}/{workspace_id}/{file_id}{file_ext}" + if workspace_id: + return f"{tenant_id}/{workspace_id}/{file_id}{file_ext}" + return f"{tenant_id}/{file_id}{file_ext}" class FileStorageService: @@ -96,7 +97,7 @@ class FileStorageService: async def upload_file( self, tenant_id: uuid.UUID, - workspace_id: uuid.UUID, + workspace_id: uuid.UUID | None, file_id: uuid.UUID, file_ext: str, content: bytes, From b9d7fb2598d8c303e4b2fad755d08a65556a9b6a Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 27 Feb 2026 10:22:36 +0800 Subject: [PATCH 13/83] [add] migration script --- .../versions/7672d8f0f939_202602271020.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 api/migrations/versions/7672d8f0f939_202602271020.py diff --git a/api/migrations/versions/7672d8f0f939_202602271020.py b/api/migrations/versions/7672d8f0f939_202602271020.py new file mode 100644 index 00000000..b99953a2 --- /dev/null +++ b/api/migrations/versions/7672d8f0f939_202602271020.py @@ -0,0 +1,36 @@ +"""202602271020 + +Revision ID: 7672d8f0f939 +Revises: 75e28690ae87 +Create Date: 2026-02-27 10:21:46.951584 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7672d8f0f939' +down_revision: Union[str, None] = '75e28690ae87' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('file_metadata', 'workspace_id', + existing_type=sa.UUID(), + nullable=True, + existing_comment='Workspace ID') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('file_metadata', 'workspace_id', + existing_type=sa.UUID(), + nullable=False, + existing_comment='Workspace ID') + # ### end Alembic commands ### From 2510f60dce981b58c4f37968ca748407a534bea4 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 27 Feb 2026 10:23:19 +0800 Subject: [PATCH 14/83] fix(web): change model list provider logo --- web/src/views/ModelManagement/List.tsx | 8 ++++---- web/src/views/ModelManagement/utils.ts | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/web/src/views/ModelManagement/List.tsx b/web/src/views/ModelManagement/List.tsx index ffa89fb4..ce4d61aa 100644 --- a/web/src/views/ModelManagement/List.tsx +++ b/web/src/views/ModelManagement/List.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:50:10 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:50:10 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-27 10:20:51 */ /** * Model List View @@ -21,7 +21,7 @@ import PageEmpty from '@/components/Empty/PageEmpty'; import Tag from '@/components/Tag'; import KeyConfigModal from './components/KeyConfigModal' import ModelListDetail from './components/ModelListDetail' -import { getLogoUrl } from './utils' +import { getListLogoUrl } from './utils' /** * Model list component @@ -70,7 +70,7 @@ const ModelList = forwardRef {item.provider[0].toUpperCase()} diff --git a/web/src/views/ModelManagement/utils.ts b/web/src/views/ModelManagement/utils.ts index fe36e137..bf44367f 100644 --- a/web/src/views/ModelManagement/utils.ts +++ b/web/src/views/ModelManagement/utils.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:50:22 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:50:22 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-27 10:22:46 */ /** * Utility functions for Model Management @@ -40,5 +40,26 @@ export const getLogoUrl = (logo?: string) => { return logo } + return ICONS[logo as keyof typeof ICONS] || undefined +} + +/** + * Get logo URL from provider name or URL + * @param provider - Provider name + * @param logo - Provider name or logo URL + * @returns Logo URL or undefined + */ +export const getListLogoUrl = (provider?: string, logo?: string) => { + let url = ICONS[provider as keyof typeof ICONS] + + if (url) return url + + if (!logo) { + return undefined + } + if (logo.startsWith('http')) { + return logo + } + return ICONS[logo as keyof typeof ICONS] || undefined } \ No newline at end of file From 1ac6702eb0cfb1fd91bd2893c3a0210975bf4c4c Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 27 Feb 2026 10:24:03 +0800 Subject: [PATCH 15/83] docs(version): Version 0.2.5 Release Notes --- api/app/version_info.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api/app/version_info.json b/api/app/version_info.json index aea03dcd..a4ff5d55 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -1,4 +1,34 @@ { + "v0.2.5": { + "introduction": { + "codeName": "行云", + "releaseDate": "2026-2-26", + "upgradePosition": "🐻 精炼根基,优化核心用户体验与系统稳定性", + "coreUpgrades": [ + "1. 用户体验与国际化 🎨
* SSO 语言参数修复:解决 SSO 认证后添加 `?language=zh` 参数仍显示英文的问题,语言偏好现正确保留
* 邮箱修改支持:用户可直接在用户管理系统中修改邮箱地址", + "2. 工作流可视化增强 💬
* 循环与迭代节点输出展示:实时显示执行进度和中间输出,便于调试复杂迭代过程
* 变量支持回车选择:支持回车键确认变量选择,简化工作流配置流程", + "3. 多租户模型管理 ⚙️
* 租户隔离的模型密钥:模型广场排除自定义模型,模型列表按租户隔离密钥,防止跨租户密钥泄露", + "4. 稳健性与缺陷修复 🔧
* 知识图谱构建修复:解决知识图谱构建流程稳定性问题,确保更可靠的实体提取和关系映射", + "
", + "版本 0.2.5 通过解决国际化边界情况、增强多租户隔离和改进工作流透明度,构建更具生产就绪性的平台。工作流可视化改进为更复杂的调试和监控能力奠定基础。未来将继续深化企业就绪性,扩展用户管理功能、优化知识图谱智能和增强工作流编排能力,在可观测性、性能优化和无缝集成模式方面持续改进。", + "智慧致远 🐻✨" + ] + }, + "introduction_en": { + "codeName": "Flowing Clouds", + "releaseDate": "2026-2-26", + "upgradePosition": "🐻 Refined foundations with enhanced user experience and system stability", + "coreUpgrades": [ + "1. User Experience & Internationalization 🎨
* SSO Language Parameter Fix: Resolved issue where `?language=zh` parameter still showed English UI after SSO authentication
* Email Update Support: Users can now modify email addresses directly in user management system", + "2. Workflow Visualization Enhancements 💬
* Loop & Iteration Node Output Display: Real-time display of execution progress and intermediate outputs for easier debugging
* Variable Selection with Enter Key: Enabled Enter key confirmation for streamlined variable assignment", + "3. Multi-Tenant Model Management ⚙️
* Tenant-Scoped Model Keys: Model marketplace excludes custom models, model list properly isolates keys per tenant to prevent cross-tenant exposure", + "4. Robustness & Bug Fixes 🔧
* Knowledge Graph Construction Fix: Addressed stability issues in knowledge graph pipeline for more reliable entity extraction and relationship mapping", + "
", + "Version 0.2.5 matures MemoryBear's operational foundations by addressing internationalization edge cases, enhancing multi-tenant isolation, and improving workflow transparency. The workflow visualization improvements lay groundwork for sophisticated debugging and monitoring capabilities. Looking forward, we will deepen enterprise readiness by expanding user management features, refining knowledge graph intelligence, and enhancing workflow orchestration with continued improvements in observability, performance optimization, and seamless integration patterns.", + "Intelligent Resilience 🐻✨" + ] + } + }, "v0.2.4": { "introduction": { "codeName": "智远", From f38223c97fb0d33bfebce2a0abf0ad234c10bb1f Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 27 Feb 2026 11:06:17 +0800 Subject: [PATCH 16/83] docs(version): Version 0.2.5 Release Notes --- api/app/version_info.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/app/version_info.json b/api/app/version_info.json index a4ff5d55..772ff56e 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -5,9 +5,9 @@ "releaseDate": "2026-2-26", "upgradePosition": "🐻 精炼根基,优化核心用户体验与系统稳定性", "coreUpgrades": [ - "1. 用户体验与国际化 🎨
* SSO 语言参数修复:解决 SSO 认证后添加 `?language=zh` 参数仍显示英文的问题,语言偏好现正确保留
* 邮箱修改支持:用户可直接在用户管理系统中修改邮箱地址", + "1. 用户体验与国际化 🎨
* 语言参数修复:语言偏好现正确保留
* 邮箱修改支持:用户可直接在用户管理系统中修改邮箱地址", "2. 工作流可视化增强 💬
* 循环与迭代节点输出展示:实时显示执行进度和中间输出,便于调试复杂迭代过程
* 变量支持回车选择:支持回车键确认变量选择,简化工作流配置流程", - "3. 多租户模型管理 ⚙️
* 租户隔离的模型密钥:模型广场排除自定义模型,模型列表按租户隔离密钥,防止跨租户密钥泄露", + "3. 优化模型管理 ⚙️
* 模型广场移除自定义模型,优化模型使用体验", "4. 稳健性与缺陷修复 🔧
* 知识图谱构建修复:解决知识图谱构建流程稳定性问题,确保更可靠的实体提取和关系映射", "
", "版本 0.2.5 通过解决国际化边界情况、增强多租户隔离和改进工作流透明度,构建更具生产就绪性的平台。工作流可视化改进为更复杂的调试和监控能力奠定基础。未来将继续深化企业就绪性,扩展用户管理功能、优化知识图谱智能和增强工作流编排能力,在可观测性、性能优化和无缝集成模式方面持续改进。", @@ -19,9 +19,9 @@ "releaseDate": "2026-2-26", "upgradePosition": "🐻 Refined foundations with enhanced user experience and system stability", "coreUpgrades": [ - "1. User Experience & Internationalization 🎨
* SSO Language Parameter Fix: Resolved issue where `?language=zh` parameter still showed English UI after SSO authentication
* Email Update Support: Users can now modify email addresses directly in user management system", + "1. User Experience & Internationalization 🎨
* Language parameter fix: language preferences are now correctly retained
* Email Update Support: Users can now modify email addresses directly in user management system", "2. Workflow Visualization Enhancements 💬
* Loop & Iteration Node Output Display: Real-time display of execution progress and intermediate outputs for easier debugging
* Variable Selection with Enter Key: Enabled Enter key confirmation for streamlined variable assignment", - "3. Multi-Tenant Model Management ⚙️
* Tenant-Scoped Model Keys: Model marketplace excludes custom models, model list properly isolates keys per tenant to prevent cross-tenant exposure", + "3. Optimized Model Management ⚙️
* Custom models have been removed from the Model marketplace to optimize the model usage experience", "4. Robustness & Bug Fixes 🔧
* Knowledge Graph Construction Fix: Addressed stability issues in knowledge graph pipeline for more reliable entity extraction and relationship mapping", "
", "Version 0.2.5 matures MemoryBear's operational foundations by addressing internationalization edge cases, enhancing multi-tenant isolation, and improving workflow transparency. The workflow visualization improvements lay groundwork for sophisticated debugging and monitoring capabilities. Looking forward, we will deepen enterprise readiness by expanding user management features, refining knowledge graph intelligence, and enhancing workflow orchestration with continued improvements in observability, performance optimization, and seamless integration patterns.", From 69af479224bb316e2ab8d2ba9af84253871efeac Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 27 Feb 2026 11:16:15 +0800 Subject: [PATCH 17/83] docs(version): Version 0.2.5 Release Notes --- api/app/version_info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/app/version_info.json b/api/app/version_info.json index 772ff56e..7d82eabc 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -10,7 +10,7 @@ "3. 优化模型管理 ⚙️
* 模型广场移除自定义模型,优化模型使用体验", "4. 稳健性与缺陷修复 🔧
* 知识图谱构建修复:解决知识图谱构建流程稳定性问题,确保更可靠的实体提取和关系映射", "
", - "版本 0.2.5 通过解决国际化边界情况、增强多租户隔离和改进工作流透明度,构建更具生产就绪性的平台。工作流可视化改进为更复杂的调试和监控能力奠定基础。未来将继续深化企业就绪性,扩展用户管理功能、优化知识图谱智能和增强工作流编排能力,在可观测性、性能优化和无缝集成模式方面持续改进。", + "版本 0.2.5 通过解决国际化边界情况和改进工作流透明度,构建更具生产就绪性的平台。工作流可视化改进为更复杂的调试和监控能力奠定基础。未来将继续深化企业就绪性,扩展用户管理功能、优化知识图谱智能和增强工作流编排能力,在可观测性、性能优化和无缝集成模式方面持续改进。", "智慧致远 🐻✨" ] }, @@ -24,7 +24,7 @@ "3. Optimized Model Management ⚙️
* Custom models have been removed from the Model marketplace to optimize the model usage experience", "4. Robustness & Bug Fixes 🔧
* Knowledge Graph Construction Fix: Addressed stability issues in knowledge graph pipeline for more reliable entity extraction and relationship mapping", "
", - "Version 0.2.5 matures MemoryBear's operational foundations by addressing internationalization edge cases, enhancing multi-tenant isolation, and improving workflow transparency. The workflow visualization improvements lay groundwork for sophisticated debugging and monitoring capabilities. Looking forward, we will deepen enterprise readiness by expanding user management features, refining knowledge graph intelligence, and enhancing workflow orchestration with continued improvements in observability, performance optimization, and seamless integration patterns.", + "Version 0.2.5 matures MemoryBear's operational foundations by addressing internationalization edge cases and improving workflow transparency. The workflow visualization improvements lay groundwork for sophisticated debugging and monitoring capabilities. Looking forward, we will deepen enterprise readiness by expanding user management features, refining knowledge graph intelligence, and enhancing workflow orchestration with continued improvements in observability, performance optimization, and seamless integration patterns.", "Intelligent Resilience 🐻✨" ] } From aef1a57ea8879b2086576ae8bc0589cbf35e3b2e Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 27 Feb 2026 12:08:18 +0800 Subject: [PATCH 18/83] fix(user): The user changes the space and modifies the role, the role information is synchronized. --- api/app/controllers/user_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/controllers/user_controller.py b/api/app/controllers/user_controller.py index 3c574c81..2806da1a 100644 --- a/api/app/controllers/user_controller.py +++ b/api/app/controllers/user_controller.py @@ -100,7 +100,7 @@ def get_current_user_info( result_schema.current_workspace_name = current_workspace.name for ws in result.workspaces: - if ws.workspace_id == current_user.current_workspace_id: + if ws.workspace_id == current_user.current_workspace_id and ws.is_active: result_schema.role = ws.role break From 37f77e099047046507f323b14fd8104c7e28f602 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 27 Feb 2026 18:48:02 +0800 Subject: [PATCH 19/83] fix(web): AutocompletePlugin key up/down support scroll --- .../Editor/plugin/AutocompletePlugin.tsx | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 8e2687f1..f9fe097e 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, type FC } from 'react'; +import { useEffect, useState, useRef, type FC } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'; @@ -22,6 +22,26 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const popupRef = useRef(null); + + const scrollSelectedIntoView = () => { + if (!popupRef.current) return; + + const selectedElement = popupRef.current.querySelector('[data-selected="true"]'); + if (!selectedElement) return; + + const container = popupRef.current; + const element = selectedElement as HTMLElement; + + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + if (elementRect.bottom > containerRect.bottom) { + container.scrollTop += elementRect.bottom - containerRect.bottom; + } else if (elementRect.top < containerRect.top) { + container.scrollTop -= containerRect.top - elementRect.top; + } + }; useEffect(() => { return editor.registerUpdateListener(({ editorState }) => { @@ -116,7 +136,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> setShowSuggestions(false); }; - const groupedSuggestions = options.reduce((groups: Record, suggestion) => { + const groupedSuggestions = options.reduce((groups: Record, suggestion) => { const { nodeData } = suggestion const nodeId = nodeData.id as string; if (!groups[nodeId]) { @@ -163,7 +183,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> while (nextIndex < allOptions.length && allOptions[nextIndex].disabled) { nextIndex++; } - return nextIndex >= allOptions.length ? prev : nextIndex; + const newIndex = nextIndex >= allOptions.length ? prev : nextIndex; + setTimeout(() => scrollSelectedIntoView(), 0); + return newIndex; }); return true; } @@ -182,7 +204,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> while (prevIndex >= 0 && allOptions[prevIndex].disabled) { prevIndex--; } - return prevIndex < 0 ? prev : prevIndex; + const newIndex = prevIndex < 0 ? prev : prevIndex; + setTimeout(() => scrollSelectedIntoView(), 0); + return newIndex; }); return true; } @@ -218,6 +242,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> } return (
e.preventDefault()} style={{ @@ -248,6 +273,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> return (
Date: Fri, 27 Feb 2026 18:59:58 +0800 Subject: [PATCH 20/83] feat(web): create space storage type add recommend --- web/src/components/RadioGroupCard/index.tsx | 7 ++++++- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/views/SpaceManagement/components/SpaceModal.tsx | 8 ++++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/components/RadioGroupCard/index.tsx b/web/src/components/RadioGroupCard/index.tsx index 41924c61..e09466cd 100644 --- a/web/src/components/RadioGroupCard/index.tsx +++ b/web/src/components/RadioGroupCard/index.tsx @@ -20,6 +20,7 @@ import { type FC, type Key, type ReactNode, useEffect } from 'react'; import { type RadioGroupProps } from 'antd'; import clsx from 'clsx' +import { useTranslation } from 'react-i18next'; /** Radio card option interface */ interface RadioCardOption { @@ -33,6 +34,8 @@ interface RadioCardOption { icon?: string; /** Whether the option is disabled */ disabled?: boolean; + /** Whether the option is recommended */ + recommend?: boolean; /** Additional properties */ [key: string]: string | number | boolean | undefined | null | Key; } @@ -63,6 +66,7 @@ const RadioGroupCard: FC = ({ allowClear = true, block = false, }) => { + const { t } = useTranslation(); /** Listen to value changes and trigger side effects via onValueChange callback */ useEffect(() => { if (onValueChange) { @@ -91,12 +95,13 @@ const RadioGroupCard: FC = ({ })}> {/* Render each option as a selectable card */} {options.map(option => ( -
handleChange(option)}> + {option.recommend &&
{t('common.recommend')}
} {/* Use custom render or default card layout */} {itemRender ? itemRender(option) : ( <> diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 8dfb68db..f2b4eaa4 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -452,6 +452,7 @@ export const en = { nextStep: 'Next Step', prevStep: 'Previous Step', exportSuccess: 'Export successful', + recommend: 'Recommend', }, model: { searchPlaceholder: 'search model…', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index feefc843..e2e7082a 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1019,6 +1019,7 @@ export const zh = { nextStep: '下一步', prevStep: '上一步', exportSuccess: '导出成功', + recommend: '推荐', }, model: { searchPlaceholder: '搜索模型…', diff --git a/web/src/views/SpaceManagement/components/SpaceModal.tsx b/web/src/views/SpaceManagement/components/SpaceModal.tsx index a0703d81..4f37b246 100644 --- a/web/src/views/SpaceManagement/components/SpaceModal.tsx +++ b/web/src/views/SpaceManagement/components/SpaceModal.tsx @@ -34,8 +34,8 @@ interface SpaceModalProps { } /** Storage types */ const types: StorageType[] = [ - 'rag', 'neo4j', + 'rag', ] /** Type icons mapping */ const typeIcons: Record = { @@ -154,6 +154,9 @@ const SpaceModal = forwardRef(({
(({ value: type, label: t(`space.${type}`), labelDesc: t(`space.${type}Desc`), - icon: typeIcons[type] + icon: typeIcons[type], + recommend: type === 'neo4j', }))} block={true} /> From dc9003f9dbc5cbf14af10a5d8d8907b0a022e596 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Sat, 28 Feb 2026 17:28:55 +0800 Subject: [PATCH 21/83] fix(web): model logo; BasicAuthLayout fix --- web/src/components/Layout/BasicAuthLayout.tsx | 10 +++++----- web/src/store/user.ts | 8 ++++---- .../ModelManagement/components/CustomModelModal.tsx | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/web/src/components/Layout/BasicAuthLayout.tsx b/web/src/components/Layout/BasicAuthLayout.tsx index a73f6c69..f279a48b 100644 --- a/web/src/components/Layout/BasicAuthLayout.tsx +++ b/web/src/components/Layout/BasicAuthLayout.tsx @@ -2,10 +2,10 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:12:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-04 14:06:28 + * @Last Modified time: 2026-02-28 17:28:41 */ /** - * BasicLayout Component + * BasicAuthLayout Component * * A minimal layout wrapper that provides: * - User information initialization @@ -26,12 +26,12 @@ import { useUser } from '@/store/user'; * Basic layout component for pages without navigation UI. * Fetches user info and storage type on mount, then renders child routes. */ -const BasicLayout: FC = () => { +const BasicAuthLayout: FC = () => { const { getUserInfo } = useUser(); // Fetch user information and storage type on component mount useEffect(() => { - getUserInfo(); + getUserInfo(undefined, true); // Pass true to skip navigation jump }, [getUserInfo]); return ( @@ -42,4 +42,4 @@ const BasicLayout: FC = () => { ) }; -export default BasicLayout; \ No newline at end of file +export default BasicAuthLayout; \ No newline at end of file diff --git a/web/src/store/user.ts b/web/src/store/user.ts index c9231d9c..f5e0cb28 100644 --- a/web/src/store/user.ts +++ b/web/src/store/user.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 16:33:54 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-04 18:30:10 + * @Last Modified time: 2026-02-28 17:21:20 */ /** * User Store @@ -44,7 +44,7 @@ export interface UserState { /** Update login information */ updateLoginInfo: (values: LoginInfo) => void; /** Get user information */ - getUserInfo: (flag?: boolean) => void; + getUserInfo: (flag?: boolean, notNeedJump?: boolean) => void; /** Clear user information */ clearUserInfo: () => void; /** Logout user */ @@ -73,13 +73,13 @@ export const useUser = create((set, get) => ({ cookieUtils.set('refreshToken', values.refresh_token); set({ loginInfo: values }); }, - getUserInfo: async (flag?: boolean) => { + getUserInfo: async (flag?: boolean, notNeedJump?: boolean) => { if (!cookieUtils.get('authToken')) { return } const { checkJump } = get() const localUser = JSON.parse(localStorage.getItem('user') || '{}') as User; - if (localUser.id) { + if (localUser.id && !notNeedJump) { checkJump() return } diff --git a/web/src/views/ModelManagement/components/CustomModelModal.tsx b/web/src/views/ModelManagement/components/CustomModelModal.tsx index fb0db96e..17373a02 100644 --- a/web/src/views/ModelManagement/components/CustomModelModal.tsx +++ b/web/src/views/ModelManagement/components/CustomModelModal.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:49:28 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:49:28 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-28 17:24:05 */ /** * Custom Model Modal @@ -50,7 +50,7 @@ const CustomModelModal = forwardRef( setModel(model); form.setFieldsValue({ ...model, - logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined + logo: model.logo && model.logo.startsWith('http') ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined }); } else { setIsEdit(false); From f5185d2e95f036754cc32d4a03f87443d1169def Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 10 Feb 2026 17:42:40 +0800 Subject: [PATCH 22/83] fix(web): FileUpload bugfix --- web/src/views/Conversation/components/FileUpload.tsx | 1 + web/src/views/Workflow/components/Chat/Chat.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index 70ee9cf2..f41fff3c 100644 --- a/web/src/views/Conversation/components/FileUpload.tsx +++ b/web/src/views/Conversation/components/FileUpload.tsx @@ -2,6 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 17:40:08 * @Last Modified time: 2026-02-11 11:32:48 */ /** diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 895ade24..aa30fc57 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-27 09:58:30 + * @Last Modified time: 2026-02-10 17:41:24 */ /** * Workflow Chat Component From e00341a4cca3e8df14cc7a8b2525c36a35789ab0 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 11 Feb 2026 11:34:20 +0800 Subject: [PATCH 23/83] fix(web): file upload bugfix --- web/src/views/Conversation/components/FileUpload.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index f41fff3c..70ee9cf2 100644 --- a/web/src/views/Conversation/components/FileUpload.tsx +++ b/web/src/views/Conversation/components/FileUpload.tsx @@ -2,7 +2,6 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-10 17:40:08 * @Last Modified time: 2026-02-11 11:32:48 */ /** From 1524d7b5ce9d5c639fa4cee9996b9d4583daf1d4 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 3 Mar 2026 15:09:16 +0800 Subject: [PATCH 24/83] fix(web): Implicit detail add check data api --- web/src/api/memory.ts | 9 +++++++-- web/src/i18n/en.ts | 3 ++- web/src/i18n/zh.ts | 3 ++- .../UserMemoryDetail/pages/ImplicitDetail.tsx | 16 ++++++++++++++-- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 987ef358..cb917ec1 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 14:00:06 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 14:00:06 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-03 14:58:32 */ import { request } from '@/utils/request' import type { @@ -163,9 +163,14 @@ export const getImplicitInterestAreas = (end_user_id: string) => { export const getImplicitHabits = (end_user_id: string) => { return request.get(`/memory/implicit-memory/habits/${end_user_id}`) } +// Implicit Memory - Generate user portrait export const generateProfile = (end_user_id: string) => { return request.post(`/memory/implicit-memory/generate_profile`, { end_user_id }) } +// Implicit Memory - Check if data exists +export const implicitCheckData = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/check-data/${end_user_id}`) +} // Short-term memory export const getShortTerm = (end_user_id: string) => { return request.get(`/memory/short/short_term`, { end_user_id }) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index f2b4eaa4..b17ad291 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2522,7 +2522,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re context_details: 'Preference Details', supporting_evidence: 'Preference Source', specific_examples: 'Source', - wordEmpty: 'Click on a node in the left chart to view preference details' + wordEmpty: 'Click on a node in the left chart to view preference details', + noData: 'Portrait data does not exist, please click the refresh button in the top right corner to initialize', }, shortTermDetail: { title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index e2e7082a..181173ff 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2518,7 +2518,8 @@ export const zh = { context_details: '偏好详情', supporting_evidence: '偏好来源', specific_examples: '来源', - wordEmpty: '点击左侧图表中的节点查看偏好详情' + wordEmpty: '点击左侧图表中的节点查看偏好详情', + noData: '画像数据不存在,请点击右上角刷新进行初始化', }, shortTermDetail: { title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。', diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx index dfe5c1ee..351e5ed1 100644 --- a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -1,6 +1,6 @@ -import { forwardRef, useImperativeHandle, useRef } from 'react' +import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Row, Col } from 'antd' +import { Row, Col, App } from 'antd' import { useParams } from 'react-router-dom' import Preferences from '../components/Preferences' @@ -9,15 +9,27 @@ import InterestAreas from '../components/InterestAreas' import Habits from '../components/Habits' import { generateProfile, + implicitCheckData, } from '@/api/memory' const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => { const { t } = useTranslation() const { id } = useParams() + const { message } = App.useApp() const preferencesRef = useRef<{ handleRefresh: () => void; }>(null) const portraitRef = useRef<{ handleRefresh: () => void; }>(null) const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null) const habitsRef = useRef<{ handleRefresh: () => void; }>(null) + + useEffect(() => { + if (!id) return + implicitCheckData(id) + .then(res => { + if (!(res as { exists: boolean }).exists) { + message.warning(t('implicitDetail.noData')) + } + }) + }, [id]) const handleRefresh = () => { if (!id) { From 6033d37537b595a904f03af19cdbe814c3dd4db4 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 15:33:17 +0800 Subject: [PATCH 25/83] [changes] Implicit and emotional memories are stored in a database. --- api/app/cache/memory/emotion_memory.py | 134 ----------- api/app/cache/memory/implicit_memory.py | 136 ----------- api/app/celery_app.py | 7 + api/app/controllers/emotion_controller.py | 114 ++++++---- .../controllers/implicit_memory_controller.py | 59 ++++- api/app/models/__init__.py | 4 +- .../models/implicit_emotions_storage_model.py | 46 ++++ .../implicit_emotions_storage_repository.py | 169 ++++++++++++++ api/app/services/emotion_analytics_service.py | 58 +++-- api/app/services/implicit_memory_service.py | 58 +++-- api/app/tasks.py | 211 +++++++++++++++++- 11 files changed, 603 insertions(+), 393 deletions(-) delete mode 100644 api/app/cache/memory/emotion_memory.py delete mode 100644 api/app/cache/memory/implicit_memory.py create mode 100644 api/app/models/implicit_emotions_storage_model.py create mode 100644 api/app/repositories/implicit_emotions_storage_repository.py diff --git a/api/app/cache/memory/emotion_memory.py b/api/app/cache/memory/emotion_memory.py deleted file mode 100644 index 45ea90de..00000000 --- a/api/app/cache/memory/emotion_memory.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -Emotion Suggestions Cache - -情绪个性化建议缓存模块 -用于缓存用户的情绪个性化建议数据 -""" -import json -import logging -from typing import Optional, Dict, Any -from datetime import datetime - -from app.aioRedis import aio_redis - -logger = logging.getLogger(__name__) - - -class EmotionMemoryCache: - """情绪建议缓存类""" - - # Key 前缀 - PREFIX = "cache:memory:emotion_memory" - - @classmethod - def _get_key(cls, *parts: str) -> str: - """生成 Redis key - - Args: - *parts: key 的各个部分 - - Returns: - 完整的 Redis key - """ - return ":".join([cls.PREFIX] + list(parts)) - - @classmethod - async def set_emotion_suggestions( - cls, - user_id: str, - suggestions_data: Dict[str, Any], - expire: int = 86400 - ) -> bool: - """设置用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - suggestions_data: 建议数据字典,包含: - - health_summary: 健康状态摘要 - - suggestions: 建议列表 - - generated_at: 生成时间(可选) - expire: 过期时间(秒),默认24小时(86400秒) - - Returns: - 是否设置成功 - """ - try: - key = cls._get_key("suggestions", user_id) - - # 添加生成时间戳 - if "generated_at" not in suggestions_data: - suggestions_data["generated_at"] = datetime.now().isoformat() - - # 添加缓存标记 - suggestions_data["cached"] = True - - value = json.dumps(suggestions_data, ensure_ascii=False) - await aio_redis.set(key, value, ex=expire) - logger.info(f"设置情绪建议缓存成功: {key}, 过期时间: {expire}秒") - return True - except Exception as e: - logger.error(f"设置情绪建议缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_emotion_suggestions(cls, user_id: str) -> Optional[Dict[str, Any]]: - """获取用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 建议数据字典,如果不存在或已过期返回 None - """ - try: - key = cls._get_key("suggestions", user_id) - value = await aio_redis.get(key) - - if value: - data = json.loads(value) - logger.info(f"成功获取情绪建议缓存: {key}") - return data - - logger.info(f"情绪建议缓存不存在或已过期: {key}") - return None - except Exception as e: - logger.error(f"获取情绪建议缓存失败: {e}", exc_info=True) - return None - - @classmethod - async def delete_emotion_suggestions(cls, user_id: str) -> bool: - """删除用户情绪建议缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 是否删除成功 - """ - try: - key = cls._get_key("suggestions", user_id) - result = await aio_redis.delete(key) - logger.info(f"删除情绪建议缓存: {key}, 结果: {result}") - return result > 0 - except Exception as e: - logger.error(f"删除情绪建议缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_suggestions_ttl(cls, user_id: str) -> int: - """获取情绪建议缓存的剩余过期时间 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 剩余秒数,-1表示永不过期,-2表示key不存在 - """ - try: - key = cls._get_key("suggestions", user_id) - ttl = await aio_redis.ttl(key) - logger.debug(f"情绪建议缓存TTL: {key} = {ttl}秒") - return ttl - except Exception as e: - logger.error(f"获取情绪建议缓存TTL失败: {e}") - return -2 diff --git a/api/app/cache/memory/implicit_memory.py b/api/app/cache/memory/implicit_memory.py deleted file mode 100644 index 21f08e9a..00000000 --- a/api/app/cache/memory/implicit_memory.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Implicit Memory Profile Cache - -隐式记忆用户画像缓存模块 -用于缓存用户的完整画像数据(偏好标签、四维画像、兴趣领域、行为习惯) -""" -import json -import logging -from typing import Optional, Dict, Any -from datetime import datetime - -from app.aioRedis import aio_redis - -logger = logging.getLogger(__name__) - - -class ImplicitMemoryCache: - """隐式记忆用户画像缓存类""" - - # Key 前缀 - PREFIX = "cache:memory:implicit_memory" - - @classmethod - def _get_key(cls, *parts: str) -> str: - """生成 Redis key - - Args: - *parts: key 的各个部分 - - Returns: - 完整的 Redis key - """ - return ":".join([cls.PREFIX] + list(parts)) - - @classmethod - async def set_user_profile( - cls, - user_id: str, - profile_data: Dict[str, Any], - expire: int = 86400 - ) -> bool: - """设置用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - profile_data: 画像数据字典,包含: - - preferences: 偏好标签列表 - - portrait: 四维画像对象 - - interest_areas: 兴趣领域分布对象 - - habits: 行为习惯列表 - - generated_at: 生成时间(可选) - expire: 过期时间(秒),默认24小时(86400秒) - - Returns: - 是否设置成功 - """ - try: - key = cls._get_key("profile", user_id) - - # 添加生成时间戳 - if "generated_at" not in profile_data: - profile_data["generated_at"] = datetime.now().isoformat() - - # 添加缓存标记 - profile_data["cached"] = True - - value = json.dumps(profile_data, ensure_ascii=False) - await aio_redis.set(key, value, ex=expire) - logger.info(f"设置用户画像缓存成功: {key}, 过期时间: {expire}秒") - return True - except Exception as e: - logger.error(f"设置用户画像缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_user_profile(cls, user_id: str) -> Optional[Dict[str, Any]]: - """获取用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 画像数据字典,如果不存在或已过期返回 None - """ - try: - key = cls._get_key("profile", user_id) - value = await aio_redis.get(key) - - if value: - data = json.loads(value) - logger.info(f"成功获取用户画像缓存: {key}") - return data - - logger.info(f"用户画像缓存不存在或已过期: {key}") - return None - except Exception as e: - logger.error(f"获取用户画像缓存失败: {e}", exc_info=True) - return None - - @classmethod - async def delete_user_profile(cls, user_id: str) -> bool: - """删除用户完整画像缓存 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 是否删除成功 - """ - try: - key = cls._get_key("profile", user_id) - result = await aio_redis.delete(key) - logger.info(f"删除用户画像缓存: {key}, 结果: {result}") - return result > 0 - except Exception as e: - logger.error(f"删除用户画像缓存失败: {e}", exc_info=True) - return False - - @classmethod - async def get_profile_ttl(cls, user_id: str) -> int: - """获取用户画像缓存的剩余过期时间 - - Args: - user_id: 用户ID(end_user_id) - - Returns: - 剩余秒数,-1表示永不过期,-2表示key不存在 - """ - try: - key = cls._get_key("profile", user_id) - ttl = await aio_redis.ttl(key) - logger.debug(f"用户画像缓存TTL: {key} = {ttl}秒") - return ttl - except Exception as e: - logger.error(f"获取用户画像缓存TTL失败: {e}") - return -2 diff --git a/api/app/celery_app.py b/api/app/celery_app.py index f422f4a0..33fa1703 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -83,6 +83,7 @@ celery_app.conf.update( 'app.tasks.regenerate_memory_cache': {'queue': 'periodic_tasks'}, 'app.tasks.run_forgetting_cycle_task': {'queue': 'periodic_tasks'}, 'app.tasks.write_all_workspaces_memory_task': {'queue': 'periodic_tasks'}, + 'app.tasks.update_implicit_emotions_storage': {'queue': 'periodic_tasks'}, }, ) @@ -95,6 +96,7 @@ memory_cache_regeneration_schedule = timedelta(hours=settings.MEMORY_CACHE_REGEN # 这个30秒的设计不合理 workspace_reflection_schedule = timedelta(seconds=30) # 每30秒运行一次settings.REFLECTION_INTERVAL_TIME forgetting_cycle_schedule = timedelta(hours=24) # 每24小时运行一次遗忘周期 +implicit_emotions_update_schedule = timedelta(hours=24) # 每24小时更新一次隐性记忆和情绪数据 #构建定时任务配置 beat_schedule_config = { @@ -120,6 +122,11 @@ beat_schedule_config = { "schedule": memory_increment_schedule, "args": (), }, + "update-implicit-emotions-storage": { + "task": "app.tasks.update_implicit_emotions_storage", + "schedule": implicit_emotions_update_schedule, + "args": (), + }, } celery_app.conf.beat_schedule = beat_schedule_config diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index eb2436d2..02ce7862 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -208,6 +208,57 @@ async def get_emotion_health( +@router.post("/check-data", response_model=ApiResponse) +async def check_emotion_data_exists( + request: EmotionSuggestionsRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """检查用户情绪建议数据是否存在 + + Args: + request: 包含 end_user_id + db: 数据库会话 + current_user: 当前用户 + + Returns: + 数据存在状态 + """ + try: + api_logger.info( + f"检查用户情绪建议数据是否存在: {request.end_user_id}", + extra={"end_user_id": request.end_user_id} + ) + + # 从数据库获取建议 + data = await emotion_service.get_cached_suggestions( + end_user_id=request.end_user_id, + db=db + ) + + if data is None: + api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") + return fail( + BizCode.NOT_FOUND, + "情绪建议数据不存在,请点击右上角刷新进行初始化", + {"exists": False} + ) + + api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") + return success(data={"exists": True}, msg="情绪建议数据已存在") + + except Exception as e: + api_logger.error( + f"检查情绪建议数据失败: {str(e)}", + extra={"end_user_id": request.end_user_id}, + exc_info=True + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"检查情绪建议数据失败: {str(e)}" + ) + + @router.post("/suggestions", response_model=ApiResponse) async def get_emotion_suggestions( request: EmotionSuggestionsRequest, @@ -215,7 +266,7 @@ async def get_emotion_suggestions( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """获取个性化情绪建议(从缓存读取) + """获取个性化情绪建议(从数据库读取) Args: request: 包含 end_user_id 和可选的 config_id @@ -223,77 +274,47 @@ async def get_emotion_suggestions( current_user: 当前用户 Returns: - 缓存的个性化情绪建议响应 + 存储的个性化情绪建议响应 """ try: # 使用集中化的语言校验 language = get_language_from_header(language_type) api_logger.info( - f"用户 {current_user.username} 请求获取个性化情绪建议(缓存)", + f"用户 {current_user.username} 请求获取个性化情绪建议", extra={ "end_user_id": request.end_user_id, "config_id": request.config_id } ) - # 从缓存获取建议 + # 从数据库获取建议 data = await emotion_service.get_cached_suggestions( end_user_id=request.end_user_id, db=db ) if data is None: - # 缓存不存在或已过期,自动触发生成 + # 数据不存在,返回提示信息 api_logger.info( - f"用户 {request.end_user_id} 的建议缓存不存在或已过期,自动生成新建议", + f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - try: - data = await emotion_service.generate_emotion_suggestions( - end_user_id=request.end_user_id, - db=db, - language=language - ) - # 保存到缓存 - await emotion_service.save_suggestions_cache( - end_user_id=request.end_user_id, - suggestions_data=data, - db=db, - expires_hours=24 - ) - except (ValueError, KeyError) as gen_e: - # 预期内的业务异常:配置缺失、数据格式问题等 - api_logger.warning( - f"自动生成建议失败(业务异常): {str(gen_e)}", - extra={"end_user_id": request.end_user_id} - ) - return fail( - BizCode.NOT_FOUND, - f"自动生成建议失败: {str(gen_e)}", - "" - ) - except Exception as gen_e: - # 非预期异常:记录完整 traceback 便于排查 - api_logger.error( - f"自动生成建议时发生未预期异常: {str(gen_e)}", - extra={"end_user_id": request.end_user_id}, - exc_info=True - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"生成建议时发生内部错误: {str(gen_e)}" - ) + return fail( + BizCode.NOT_FOUND, + "情绪建议数据不存在,请点击右上角刷新进行初始化", + "" + ) api_logger.info( - "个性化建议获取成功(缓存)", + "个性化建议获取成功", extra={ "end_user_id": request.end_user_id, "suggestions_count": len(data.get("suggestions", [])) } ) - return success(data=data, msg="个性化建议获取成功(缓存)") + return success(data=data, msg="个性化建议获取成功") except Exception as e: api_logger.error( @@ -314,7 +335,7 @@ async def generate_emotion_suggestions( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """生成个性化情绪建议(调用LLM并缓存) + """生成个性化情绪建议(调用LLM并保存到数据库) Args: request: 包含 end_user_id @@ -342,12 +363,11 @@ async def generate_emotion_suggestions( language=language ) - # 保存到缓存 + # 保存到数据库 await emotion_service.save_suggestions_cache( end_user_id=request.end_user_id, suggestions_data=data, - db=db, - expires_hours=24 + db=db ) api_logger.info( diff --git a/api/app/controllers/implicit_memory_controller.py b/api/app/controllers/implicit_memory_controller.py index 96e437d6..91e634c9 100644 --- a/api/app/controllers/implicit_memory_controller.py +++ b/api/app/controllers/implicit_memory_controller.py @@ -122,6 +122,49 @@ def validate_confidence_threshold(threshold: float) -> None: raise ValueError("confidence_threshold must be between 0.0 and 1.0") +@router.get("/check-data/{end_user_id}", response_model=ApiResponse) +@cur_workspace_access_guard() +async def check_user_data_exists( + end_user_id: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +) -> ApiResponse: + """ + 检查用户画像数据是否存在 + + Args: + end_user_id: 目标用户ID + + Returns: + 数据存在状态 + """ + api_logger.info(f"检查用户画像数据是否存在: {end_user_id}") + + try: + # Validate inputs + validate_user_id(end_user_id) + + # Create service with user-specific config + service = ImplicitMemoryService(db=db, end_user_id=end_user_id) + + # Get cached profile + cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) + + if cached_profile is None: + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") + return fail( + BizCode.NOT_FOUND, + "画像数据不存在,请点击右上角刷新进行初始化", + {"exists": False} + ) + + api_logger.info(f"用户 {end_user_id} 的画像数据存在") + return success(data={"exists": True}, msg="画像数据已存在") + + except Exception as e: + return handle_implicit_memory_error(e, "检查画像数据", end_user_id) + + @router.get("/preferences/{end_user_id}", response_model=ApiResponse) @cur_workspace_access_guard() async def get_preference_tags( @@ -159,10 +202,10 @@ async def get_preference_tags( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -230,10 +273,10 @@ async def get_dimension_portrait( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -278,10 +321,10 @@ async def get_interest_area_distribution( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) @@ -330,10 +373,10 @@ async def get_behavior_habits( cached_profile = await service.get_cached_profile(end_user_id=end_user_id, db=db) if cached_profile is None: - api_logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + api_logger.info(f"用户 {end_user_id} 的画像数据不存在") return fail( BizCode.NOT_FOUND, - "画像缓存不存在或已过期,请右上角刷新生成新画像", + "画像数据不存在,请点击右上角刷新进行初始化", "" ) diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index b1b723e9..c6098a6d 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -35,6 +35,7 @@ from .ontology_scene import OntologyScene from .ontology_class import OntologyClass from .ontology_scene import OntologyScene from .ontology_class import OntologyClass +from .implicit_emotions_storage_model import ImplicitEmotionsStorage __all__ = [ "Tenants", @@ -90,5 +91,6 @@ __all__ = [ "MemoryPerceptualModel", "ModelBase", "LoadBalanceStrategy", - "Skill" + "Skill", + "ImplicitEmotionsStorage" ] diff --git a/api/app/models/implicit_emotions_storage_model.py b/api/app/models/implicit_emotions_storage_model.py new file mode 100644 index 00000000..57c0fd61 --- /dev/null +++ b/api/app/models/implicit_emotions_storage_model.py @@ -0,0 +1,46 @@ +""" +Implicit Emotions Storage Model + +数据库模型:存储用户的隐性记忆画像和情绪建议数据 +替代原有的Redis缓存方式 +""" +import uuid +from datetime import datetime +from sqlalchemy import Column, String, Text, DateTime, Index +from sqlalchemy.dialects.postgresql import UUID, JSONB +from app.db import Base + + +class ImplicitEmotionsStorage(Base): + """隐性记忆和情绪存储表""" + + __tablename__ = "implicit_emotions_storage" + + # 主键 + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="主键ID") + + # 用户标识 + end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID") + + # 隐性记忆画像数据(JSON格式) + implicit_profile = Column(JSONB, nullable=True, comment="隐性记忆用户画像数据") + + # 情绪建议数据(JSON格式) + emotion_suggestions = Column(JSONB, nullable=True, comment="情绪个性化建议数据") + + # 时间戳 + created_at = Column(DateTime, nullable=False, default=datetime.utcnow, comment="创建时间") + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间") + + # 数据生成时间(用于业务逻辑) + implicit_generated_at = Column(DateTime, nullable=True, comment="隐性记忆画像生成时间") + emotion_generated_at = Column(DateTime, nullable=True, comment="情绪建议生成时间") + + # 索引 + __table_args__ = ( + Index('idx_end_user_id', 'end_user_id'), + Index('idx_updated_at', 'updated_at'), + ) + + def __repr__(self): + return f"" diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py new file mode 100644 index 00000000..fd4b10ce --- /dev/null +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -0,0 +1,169 @@ +""" +Implicit Emotions Storage Repository + +数据访问层:处理隐性记忆和情绪数据的数据库操作 +""" +import logging +from datetime import datetime +from typing import Optional, List +from sqlalchemy.orm import Session +from sqlalchemy import select + +from app.models.implicit_emotions_storage_model import ImplicitEmotionsStorage + +logger = logging.getLogger(__name__) + + +class ImplicitEmotionsStorageRepository: + """隐性记忆和情绪存储仓储类""" + + def __init__(self, db: Session): + self.db = db + + def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitEmotionsStorage]: + """根据终端用户ID获取存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 存储记录,如果不存在返回None + """ + try: + stmt = select(ImplicitEmotionsStorage).where( + ImplicitEmotionsStorage.end_user_id == end_user_id + ) + result = self.db.execute(stmt).scalar_one_or_none() + return result + except Exception as e: + logger.error(f"获取用户存储记录失败: end_user_id={end_user_id}, error={e}") + return None + + def create(self, end_user_id: str) -> ImplicitEmotionsStorage: + """创建新的存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 新创建的存储记录 + """ + try: + storage = ImplicitEmotionsStorage( + end_user_id=end_user_id, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + self.db.add(storage) + self.db.commit() + self.db.refresh(storage) + logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"创建用户存储记录失败: end_user_id={end_user_id}, error={e}") + raise + + def update_implicit_profile( + self, + end_user_id: str, + profile_data: dict + ) -> Optional[ImplicitEmotionsStorage]: + """更新隐性记忆画像数据 + + Args: + end_user_id: 终端用户ID + profile_data: 画像数据 + + Returns: + 更新后的存储记录 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + + if storage is None: + # 如果记录不存在,创建新记录 + storage = self.create(end_user_id) + + storage.implicit_profile = profile_data + storage.implicit_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(storage) + logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"更新隐性记忆画像失败: end_user_id={end_user_id}, error={e}") + raise + + def update_emotion_suggestions( + self, + end_user_id: str, + suggestions_data: dict + ) -> Optional[ImplicitEmotionsStorage]: + """更新情绪建议数据 + + Args: + end_user_id: 终端用户ID + suggestions_data: 建议数据 + + Returns: + 更新后的存储记录 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + + if storage is None: + # 如果记录不存在,创建新记录 + storage = self.create(end_user_id) + + storage.emotion_suggestions = suggestions_data + storage.emotion_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(storage) + logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") + return storage + except Exception as e: + self.db.rollback() + logger.error(f"更新情绪建议失败: end_user_id={end_user_id}, error={e}") + raise + + def get_all_user_ids(self) -> List[str]: + """获取所有已存储数据的用户ID列表 + + Returns: + 用户ID列表 + """ + try: + stmt = select(ImplicitEmotionsStorage.end_user_id) + result = self.db.execute(stmt).scalars().all() + return list(result) + except Exception as e: + logger.error(f"获取所有用户ID失败: error={e}") + return [] + + def delete_by_end_user_id(self, end_user_id: str) -> bool: + """删除用户的存储记录 + + Args: + end_user_id: 终端用户ID + + Returns: + 是否删除成功 + """ + try: + storage = self.get_by_end_user_id(end_user_id) + if storage: + self.db.delete(storage) + self.db.commit() + logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") + return True + return False + except Exception as e: + self.db.rollback() + logger.error(f"删除用户存储记录失败: end_user_id={end_user_id}, error={e}") + return False diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index 89e3cab9..099cbfb7 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -843,32 +843,33 @@ class EmotionAnalyticsService: end_user_id: str, db: Session, ) -> Optional[Dict[str, Any]]: - """从 Redis 缓存获取个性化情绪建议 + """从数据库获取个性化情绪建议 Args: end_user_id: 宿主ID(用户组ID) - db: 数据库会话(保留参数以保持接口兼容性) + db: 数据库会话 Returns: - Dict: 缓存的建议数据,如果不存在或已过期返回 None + Dict: 存储的建议数据,如果不存在返回 None """ try: - from app.cache.memory.emotion_memory import EmotionMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}") + logger.info(f"尝试从数据库获取情绪建议: user={end_user_id}") - # 从 Redis 获取缓存 - cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id) + # 从数据库获取存储记录 + repo = ImplicitEmotionsStorageRepository(db) + storage = repo.get_by_end_user_id(end_user_id) - if cached_data is None: - logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期") + if storage is None or storage.emotion_suggestions is None: + logger.info(f"用户 {end_user_id} 的建议数据不存在") return None - logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}") - return cached_data + logger.info(f"成功从数据库获取建议: user={end_user_id}") + return storage.emotion_suggestions except Exception as e: - logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True) + logger.error(f"从数据库获取建议失败: {str(e)}", exc_info=True) return None async def save_suggestions_cache( @@ -876,36 +877,27 @@ class EmotionAnalyticsService: end_user_id: str, suggestions_data: Dict[str, Any], db: Session, - expires_hours: int = 24 + expires_hours: int = 24 # 参数保留以保持接口兼容性 ) -> None: - """保存建议到 Redis 缓存 + """保存建议到数据库 Args: end_user_id: 宿主ID(用户组ID) suggestions_data: 建议数据 - db: 数据库会话(保留参数以保持接口兼容性) - expires_hours: 过期时间(小时),默认24小时 + db: 数据库会话 + expires_hours: 保留参数(兼容性) """ try: - from app.cache.memory.emotion_memory import EmotionMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时") + logger.info(f"保存建议到数据库: user={end_user_id}") - # 计算过期时间(秒) - expire_seconds = expires_hours * 3600 + # 保存到数据库 + repo = ImplicitEmotionsStorageRepository(db) + repo.update_emotion_suggestions(end_user_id, suggestions_data) - # 保存到 Redis - success = await EmotionMemoryCache.set_emotion_suggestions( - user_id=end_user_id, - suggestions_data=suggestions_data, - expire=expire_seconds - ) - - if success: - logger.info(f"建议缓存保存成功: user={end_user_id}") - else: - logger.warning(f"建议缓存保存失败: user={end_user_id}") + logger.info(f"建议保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True) - # 不抛出异常,缓存失败不应影响主流程 \ No newline at end of file + logger.error(f"保存建议失败: {str(e)}", exc_info=True) + # 不抛出异常,存储失败不应影响主流程 \ No newline at end of file diff --git a/api/app/services/implicit_memory_service.py b/api/app/services/implicit_memory_service.py index 34ebe880..534f138c 100644 --- a/api/app/services/implicit_memory_service.py +++ b/api/app/services/implicit_memory_service.py @@ -422,32 +422,33 @@ class ImplicitMemoryService: end_user_id: str, db: Session ) -> Optional[dict]: - """从 Redis 缓存获取完整用户画像 + """从数据库获取完整用户画像 Args: end_user_id: 终端用户ID - db: 数据库会话(保留参数以保持接口兼容性) + db: 数据库会话 Returns: - Dict: 缓存的画像数据,如果不存在或已过期返回 None + Dict: 存储的画像数据,如果不存在返回 None """ try: - from app.cache.memory.implicit_memory import ImplicitMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"尝试从 Redis 缓存获取用户画像: user={end_user_id}") + logger.info(f"尝试从数据库获取用户画像: user={end_user_id}") - # 从 Redis 获取缓存 - cached_data = await ImplicitMemoryCache.get_user_profile(end_user_id) + # 从数据库获取存储记录 + repo = ImplicitEmotionsStorageRepository(db) + storage = repo.get_by_end_user_id(end_user_id) - if cached_data is None: - logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期") + if storage is None or storage.implicit_profile is None: + logger.info(f"用户 {end_user_id} 的画像数据不存在") return None - logger.info(f"成功从 Redis 缓存获取用户画像: user={end_user_id}") - return cached_data + logger.info(f"成功从数据库获取用户画像: user={end_user_id}") + return storage.implicit_profile except Exception as e: - logger.error(f"从 Redis 缓存获取用户画像失败: {str(e)}", exc_info=True) + logger.error(f"从数据库获取用户画像失败: {str(e)}", exc_info=True) return None async def save_profile_cache( @@ -455,36 +456,27 @@ class ImplicitMemoryService: end_user_id: str, profile_data: dict, db: Session, - expires_hours: int = 168 # 默认7天 + expires_hours: int = 168 # 参数保留以保持接口兼容性 ) -> None: - """保存用户画像到 Redis 缓存 + """保存用户画像到数据库 Args: end_user_id: 终端用户ID profile_data: 画像数据 - db: 数据库会话(保留参数以保持接口兼容性) - expires_hours: 过期时间(小时),默认168小时(7天) + db: 数据库会话 + expires_hours: 保留参数(兼容性) """ try: - from app.cache.memory.implicit_memory import ImplicitMemoryCache + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository - logger.info(f"保存用户画像到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时") + logger.info(f"保存用户画像到数据库: user={end_user_id}") - # 计算过期时间(秒) - expire_seconds = expires_hours * 3600 + # 保存到数据库 + repo = ImplicitEmotionsStorageRepository(db) + repo.update_implicit_profile(end_user_id, profile_data) - # 保存到 Redis - success = await ImplicitMemoryCache.set_user_profile( - user_id=end_user_id, - profile_data=profile_data, - expire=expire_seconds - ) - - if success: - logger.info(f"用户画像缓存保存成功: user={end_user_id}") - else: - logger.warning(f"用户画像缓存保存失败: user={end_user_id}") + logger.info(f"用户画像保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True) - # 不抛出异常,缓存失败不应影响主流程 + logger.error(f"保存用户画像失败: {str(e)}", exc_info=True) + # 不抛出异常,存储失败不应影响主流程 diff --git a/api/app/tasks.py b/api/app/tasks.py index 8e3aea85..67498b85 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -2121,4 +2121,213 @@ def run_forgetting_cycle_task(self, config_id: Optional[uuid.UUID] = None) -> Di # "config_id": config_id, # "elapsed_time": elapsed_time, # "task_id": self.request.id -# } \ No newline at end of file +# } + + +# ============================================================================= +# 隐性记忆和情绪数据更新定时任务 +# ============================================================================= + +@celery_app.task( + name="app.tasks.update_implicit_emotions_storage", + bind=True, + ignore_result=True, + max_retries=0, + acks_late=False, + time_limit=7200, # 2小时硬超时 + soft_time_limit=6900, # 1小时55分钟软超时 +) +def update_implicit_emotions_storage(self) -> Dict[str, Any]: + """定时任务:更新所有用户的隐性记忆画像和情绪建议数据 + + 遍历数据库中所有已存在数据的用户,为每个用户重新生成隐性记忆画像和情绪建议。 + 实现错误隔离,单个用户失败不影响其他用户的处理。 + + Returns: + 包含任务执行结果的字典,包括: + - status: 任务状态 (SUCCESS/FAILURE) + - message: 执行消息 + - total_users: 总用户数 + - successful_implicit: 成功更新隐性记忆的用户数 + - successful_emotion: 成功更新情绪建议的用户数 + - failed: 失败的用户数 + - user_results: 每个用户的详细结果 + - elapsed_time: 执行耗时(秒) + - task_id: 任务ID + """ + start_time = time.time() + + async def _run() -> Dict[str, Any]: + from app.core.logging_config import get_logger + from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository + from app.services.implicit_memory_service import ImplicitMemoryService + from app.services.emotion_analytics_service import EmotionAnalyticsService + + logger = get_logger(__name__) + logger.info("开始执行隐性记忆和情绪数据更新定时任务") + + total_users = 0 + successful_implicit = 0 + successful_emotion = 0 + failed = 0 + user_results = [] + + with get_db_context() as db: + try: + # 获取所有已存储数据的用户ID + repo = ImplicitEmotionsStorageRepository(db) + user_ids = repo.get_all_user_ids() + total_users = len(user_ids) + + logger.info(f"找到 {total_users} 个需要更新的用户") + + # 遍历每个用户并更新数据 + for end_user_id in user_ids: + logger.info(f"开始处理用户: {end_user_id}") + user_start_time = time.time() + + implicit_success = False + emotion_success = False + errors = [] + + try: + # 更新隐性记忆画像 + try: + implicit_service = ImplicitMemoryService(db=db, end_user_id=end_user_id) + profile_data = await implicit_service.generate_complete_profile(user_id=end_user_id) + await implicit_service.save_profile_cache( + end_user_id=end_user_id, + profile_data=profile_data, + db=db + ) + implicit_success = True + logger.info(f"成功更新用户 {end_user_id} 的隐性记忆画像") + except Exception as e: + error_msg = f"隐性记忆更新失败: {str(e)}" + errors.append(error_msg) + logger.error(f"用户 {end_user_id} {error_msg}") + + # 更新情绪建议 + try: + emotion_service = EmotionAnalyticsService(db=db, end_user_id=end_user_id) + suggestions_data = await emotion_service.generate_emotion_suggestions( + end_user_id=end_user_id, + db=db, + language="zh" + ) + await emotion_service.save_suggestions_cache( + end_user_id=end_user_id, + suggestions_data=suggestions_data, + db=db + ) + emotion_success = True + logger.info(f"成功更新用户 {end_user_id} 的情绪建议") + except Exception as e: + error_msg = f"情绪建议更新失败: {str(e)}" + errors.append(error_msg) + logger.error(f"用户 {end_user_id} {error_msg}") + + # 统计结果 + if implicit_success: + successful_implicit += 1 + if emotion_success: + successful_emotion += 1 + if not implicit_success and not emotion_success: + failed += 1 + + user_elapsed = time.time() - user_start_time + + # 记录用户处理结果 + user_result = { + "end_user_id": end_user_id, + "implicit_success": implicit_success, + "emotion_success": emotion_success, + "errors": errors, + "elapsed_time": user_elapsed + } + user_results.append(user_result) + + logger.info( + f"用户 {end_user_id} 处理完成: " + f"隐性记忆={'成功' if implicit_success else '失败'}, " + f"情绪建议={'成功' if emotion_success else '失败'}, " + f"耗时={user_elapsed:.2f}秒" + ) + + except Exception as e: + # 单个用户失败不影响其他用户(错误隔离) + failed += 1 + user_elapsed = time.time() - user_start_time + error_info = { + "end_user_id": end_user_id, + "implicit_success": False, + "emotion_success": False, + "errors": [str(e)], + "elapsed_time": user_elapsed + } + user_results.append(error_info) + logger.error(f"处理用户 {end_user_id} 时出错: {str(e)}") + + # 记录总体统计信息 + logger.info( + f"隐性记忆和情绪数据更新定时任务完成: " + f"总用户数={total_users}, " + f"隐性记忆成功={successful_implicit}, " + f"情绪建议成功={successful_emotion}, " + f"失败={failed}" + ) + + return { + "status": "SUCCESS", + "message": f"成功处理 {total_users} 个用户,隐性记忆 {successful_implicit} 个成功,情绪建议 {successful_emotion} 个成功", + "total_users": total_users, + "successful_implicit": successful_implicit, + "successful_emotion": successful_emotion, + "failed": failed, + "user_results": user_results[:50] # 只保留前50个用户的详细结果 + } + + except Exception as e: + logger.error(f"隐性记忆和情绪数据更新定时任务执行失败: {str(e)}") + return { + "status": "FAILURE", + "error": str(e), + "total_users": total_users, + "successful_implicit": successful_implicit, + "successful_emotion": successful_emotion, + "failed": failed, + "user_results": user_results[:50] + } + + try: + # 使用 nest_asyncio 来避免事件循环冲突 + try: + import nest_asyncio + nest_asyncio.apply() + except ImportError: + pass + + # 尝试获取现有事件循环,如果不存在则创建新的 + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + result = loop.run_until_complete(_run()) + elapsed_time = time.time() - start_time + result["elapsed_time"] = elapsed_time + result["task_id"] = self.request.id + + return result + except Exception as e: + elapsed_time = time.time() - start_time + return { + "status": "FAILURE", + "error": str(e), + "elapsed_time": elapsed_time, + "task_id": self.request.id + } From 7446241735f7df69e58e119c6512596cfe338a15 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 15:16:47 +0800 Subject: [PATCH 26/83] [changes] AI reviews and modifies the code --- api/app/cache/__init__.py | 7 +- api/app/cache/memory/__init__.py | 8 +- api/app/controllers/emotion_controller.py | 12 +- .../controllers/implicit_memory_controller.py | 31 +-- .../models/implicit_emotions_storage_model.py | 7 +- .../implicit_emotions_storage_repository.py | 212 +++++++----------- api/app/services/emotion_analytics_service.py | 6 +- api/app/services/implicit_memory_service.py | 4 +- api/app/tasks.py | 15 +- 9 files changed, 114 insertions(+), 188 deletions(-) diff --git a/api/app/cache/__init__.py b/api/app/cache/__init__.py index a79d4cb2..748ce8ae 100644 --- a/api/app/cache/__init__.py +++ b/api/app/cache/__init__.py @@ -2,10 +2,7 @@ Cache 缓存模块 提供各种缓存功能的统一入口 +注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .memory import EmotionMemoryCache, ImplicitMemoryCache -__all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", -] +__all__ = [] diff --git a/api/app/cache/memory/__init__.py b/api/app/cache/memory/__init__.py index 4ada3153..35f45aad 100644 --- a/api/app/cache/memory/__init__.py +++ b/api/app/cache/memory/__init__.py @@ -2,11 +2,7 @@ Memory 缓存模块 提供记忆系统相关的缓存功能 +注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .emotion_memory import EmotionMemoryCache -from .implicit_memory import ImplicitMemoryCache -__all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", -] +__all__ = [] diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 02ce7862..0a8b5fc8 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -262,7 +262,6 @@ async def check_emotion_data_exists( @router.post("/suggestions", response_model=ApiResponse) async def get_emotion_suggestions( request: EmotionSuggestionsRequest, - language_type: str = Header(default=None, alias="X-Language-Type"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): @@ -277,9 +276,6 @@ async def get_emotion_suggestions( 存储的个性化情绪建议响应 """ try: - # 使用集中化的语言校验 - language = get_language_from_header(language_type) - api_logger.info( f"用户 {current_user.username} 请求获取个性化情绪建议", extra={ @@ -295,15 +291,13 @@ async def get_emotion_suggestions( ) if data is None: - # 数据不存在,返回提示信息 api_logger.info( f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - return fail( - BizCode.NOT_FOUND, - "情绪建议数据不存在,请点击右上角刷新进行初始化", - "" + return success( + data={"exists": False}, + msg="情绪建议数据不存在,请点击右上角刷新进行初始化" ) api_logger.info( diff --git a/api/app/controllers/implicit_memory_controller.py b/api/app/controllers/implicit_memory_controller.py index 91e634c9..76a87c5f 100644 --- a/api/app/controllers/implicit_memory_controller.py +++ b/api/app/controllers/implicit_memory_controller.py @@ -152,10 +152,9 @@ async def check_user_data_exists( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - {"exists": False} + return success( + data={"exists": False}, + msg="画像数据不存在,请点击右上角刷新进行初始化" ) api_logger.info(f"用户 {end_user_id} 的画像数据存在") @@ -203,11 +202,7 @@ async def get_preference_tags( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract preferences from cache preferences = cached_profile.get("preferences", []) @@ -274,11 +269,7 @@ async def get_dimension_portrait( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract portrait from cache portrait = cached_profile.get("portrait", {}) @@ -322,11 +313,7 @@ async def get_interest_area_distribution( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract interest areas from cache interest_areas = cached_profile.get("interest_areas", {}) @@ -374,11 +361,7 @@ async def get_behavior_habits( if cached_profile is None: api_logger.info(f"用户 {end_user_id} 的画像数据不存在") - return fail( - BizCode.NOT_FOUND, - "画像数据不存在,请点击右上角刷新进行初始化", - "" - ) + return fail(BizCode.NOT_FOUND, "", "") # Extract habits from cache habits = cached_profile.get("habits", []) diff --git a/api/app/models/implicit_emotions_storage_model.py b/api/app/models/implicit_emotions_storage_model.py index 57c0fd61..cf654950 100644 --- a/api/app/models/implicit_emotions_storage_model.py +++ b/api/app/models/implicit_emotions_storage_model.py @@ -19,8 +19,8 @@ class ImplicitEmotionsStorage(Base): # 主键 id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, comment="主键ID") - # 用户标识 - end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID") + # 用户标识(unique=True会自动创建唯一索引) + end_user_id = Column(String(255), nullable=False, unique=True, comment="终端用户ID") # 隐性记忆画像数据(JSON格式) implicit_profile = Column(JSONB, nullable=True, comment="隐性记忆用户画像数据") @@ -36,9 +36,8 @@ class ImplicitEmotionsStorage(Base): implicit_generated_at = Column(DateTime, nullable=True, comment="隐性记忆画像生成时间") emotion_generated_at = Column(DateTime, nullable=True, comment="情绪建议生成时间") - # 索引 + # 索引(只为updated_at创建索引,end_user_id的unique约束已自动创建索引) __table_args__ = ( - Index('idx_end_user_id', 'end_user_id'), Index('idx_updated_at', 'updated_at'), ) diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py index fd4b10ce..176012b7 100644 --- a/api/app/repositories/implicit_emotions_storage_repository.py +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -2,10 +2,11 @@ Implicit Emotions Storage Repository 数据访问层:处理隐性记忆和情绪数据的数据库操作 +事务由调用方控制,仓储层只使用 flush/refresh """ import logging from datetime import datetime -from typing import Optional, List +from typing import Optional, Generator from sqlalchemy.orm import Session from sqlalchemy import select @@ -16,154 +17,105 @@ logger = logging.getLogger(__name__) class ImplicitEmotionsStorageRepository: """隐性记忆和情绪存储仓储类""" - + def __init__(self, db: Session): self.db = db - + def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitEmotionsStorage]: - """根据终端用户ID获取存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 存储记录,如果不存在返回None - """ + """根据终端用户ID获取存储记录""" try: stmt = select(ImplicitEmotionsStorage).where( ImplicitEmotionsStorage.end_user_id == end_user_id ) - result = self.db.execute(stmt).scalar_one_or_none() - return result + return self.db.execute(stmt).scalar_one_or_none() except Exception as e: logger.error(f"获取用户存储记录失败: end_user_id={end_user_id}, error={e}") return None - + def create(self, end_user_id: str) -> ImplicitEmotionsStorage: - """创建新的存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 新创建的存储记录 - """ - try: - storage = ImplicitEmotionsStorage( - end_user_id=end_user_id, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow() - ) - self.db.add(storage) - self.db.commit() - self.db.refresh(storage) - logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"创建用户存储记录失败: end_user_id={end_user_id}, error={e}") - raise - + """创建新的存储记录(事务由调用方提交)""" + storage = ImplicitEmotionsStorage( + end_user_id=end_user_id, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + self.db.add(storage) + self.db.flush() + self.db.refresh(storage) + logger.info(f"创建用户存储记录成功: end_user_id={end_user_id}") + return storage + def update_implicit_profile( self, end_user_id: str, profile_data: dict - ) -> Optional[ImplicitEmotionsStorage]: - """更新隐性记忆画像数据 - - Args: - end_user_id: 终端用户ID - profile_data: 画像数据 - - Returns: - 更新后的存储记录 - """ - try: - storage = self.get_by_end_user_id(end_user_id) - - if storage is None: - # 如果记录不存在,创建新记录 - storage = self.create(end_user_id) - - storage.implicit_profile = profile_data - storage.implicit_generated_at = datetime.utcnow() - storage.updated_at = datetime.utcnow() - - self.db.commit() - self.db.refresh(storage) - logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"更新隐性记忆画像失败: end_user_id={end_user_id}, error={e}") - raise - + ) -> ImplicitEmotionsStorage: + """更新隐性记忆画像数据(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage is None: + storage = self.create(end_user_id) + + storage.implicit_profile = profile_data + storage.implicit_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.flush() + self.db.refresh(storage) + logger.info(f"更新隐性记忆画像成功: end_user_id={end_user_id}") + return storage + def update_emotion_suggestions( self, end_user_id: str, suggestions_data: dict - ) -> Optional[ImplicitEmotionsStorage]: - """更新情绪建议数据 - + ) -> ImplicitEmotionsStorage: + """更新情绪建议数据(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage is None: + storage = self.create(end_user_id) + + storage.emotion_suggestions = suggestions_data + storage.emotion_generated_at = datetime.utcnow() + storage.updated_at = datetime.utcnow() + + self.db.flush() + self.db.refresh(storage) + logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") + return storage + + def get_all_user_ids(self, batch_size: int = 100) -> Generator[str, None, None]: + """分批次获取所有已存储数据的用户ID(避免大数据量内存溢出) + Args: - end_user_id: 终端用户ID - suggestions_data: 建议数据 - - Returns: - 更新后的存储记录 + batch_size: 每批次加载的数量,默认100 + + Yields: + 用户ID字符串 """ - try: - storage = self.get_by_end_user_id(end_user_id) - - if storage is None: - # 如果记录不存在,创建新记录 - storage = self.create(end_user_id) - - storage.emotion_suggestions = suggestions_data - storage.emotion_generated_at = datetime.utcnow() - storage.updated_at = datetime.utcnow() - - self.db.commit() - self.db.refresh(storage) - logger.info(f"更新情绪建议成功: end_user_id={end_user_id}") - return storage - except Exception as e: - self.db.rollback() - logger.error(f"更新情绪建议失败: end_user_id={end_user_id}, error={e}") - raise - - def get_all_user_ids(self) -> List[str]: - """获取所有已存储数据的用户ID列表 - - Returns: - 用户ID列表 - """ - try: - stmt = select(ImplicitEmotionsStorage.end_user_id) - result = self.db.execute(stmt).scalars().all() - return list(result) - except Exception as e: - logger.error(f"获取所有用户ID失败: error={e}") - return [] - + offset = 0 + while True: + try: + stmt = ( + select(ImplicitEmotionsStorage.end_user_id) + .order_by(ImplicitEmotionsStorage.end_user_id) + .limit(batch_size) + .offset(offset) + ) + batch = self.db.execute(stmt).scalars().all() + if not batch: + break + yield from batch + offset += batch_size + except Exception as e: + logger.error(f"分批获取用户ID失败: offset={offset}, error={e}") + break + def delete_by_end_user_id(self, end_user_id: str) -> bool: - """删除用户的存储记录 - - Args: - end_user_id: 终端用户ID - - Returns: - 是否删除成功 - """ - try: - storage = self.get_by_end_user_id(end_user_id) - if storage: - self.db.delete(storage) - self.db.commit() - logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") - return True - return False - except Exception as e: - self.db.rollback() - logger.error(f"删除用户存储记录失败: end_user_id={end_user_id}, error={e}") - return False + """删除用户的存储记录(事务由调用方提交)""" + storage = self.get_by_end_user_id(end_user_id) + if storage: + self.db.delete(storage) + self.db.flush() + logger.info(f"删除用户存储记录成功: end_user_id={end_user_id}") + return True + return False diff --git a/api/app/services/emotion_analytics_service.py b/api/app/services/emotion_analytics_service.py index 099cbfb7..c226348e 100644 --- a/api/app/services/emotion_analytics_service.py +++ b/api/app/services/emotion_analytics_service.py @@ -892,12 +892,12 @@ class EmotionAnalyticsService: logger.info(f"保存建议到数据库: user={end_user_id}") - # 保存到数据库 repo = ImplicitEmotionsStorageRepository(db) repo.update_emotion_suggestions(end_user_id, suggestions_data) + db.commit() logger.info(f"建议保存成功: user={end_user_id}") except Exception as e: - logger.error(f"保存建议失败: {str(e)}", exc_info=True) - # 不抛出异常,存储失败不应影响主流程 \ No newline at end of file + db.rollback() + logger.error(f"保存建议失败: {str(e)}", exc_info=True) \ No newline at end of file diff --git a/api/app/services/implicit_memory_service.py b/api/app/services/implicit_memory_service.py index 534f138c..4bd11deb 100644 --- a/api/app/services/implicit_memory_service.py +++ b/api/app/services/implicit_memory_service.py @@ -471,12 +471,12 @@ class ImplicitMemoryService: logger.info(f"保存用户画像到数据库: user={end_user_id}") - # 保存到数据库 repo = ImplicitEmotionsStorageRepository(db) repo.update_implicit_profile(end_user_id, profile_data) + db.commit() logger.info(f"用户画像保存成功: user={end_user_id}") except Exception as e: + db.rollback() logger.error(f"保存用户画像失败: {str(e)}", exc_info=True) - # 不抛出异常,存储失败不应影响主流程 diff --git a/api/app/tasks.py b/api/app/tasks.py index 67498b85..877224b7 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -2160,6 +2160,8 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: async def _run() -> Dict[str, Any]: from app.core.logging_config import get_logger from app.repositories.implicit_emotions_storage_repository import ImplicitEmotionsStorageRepository + from app.models.implicit_emotions_storage_model import ImplicitEmotionsStorage + from sqlalchemy import select, func from app.services.implicit_memory_service import ImplicitMemoryService from app.services.emotion_analytics_service import EmotionAnalyticsService @@ -2174,15 +2176,18 @@ def update_implicit_emotions_storage(self) -> Dict[str, Any]: with get_db_context() as db: try: - # 获取所有已存储数据的用户ID + # 获取所有已存储数据的用户ID(分批次处理) repo = ImplicitEmotionsStorageRepository(db) - user_ids = repo.get_all_user_ids() - total_users = len(user_ids) + # 先统计总数用于日志 + from sqlalchemy import func + total_users = db.execute( + select(func.count()).select_from(ImplicitEmotionsStorage) + ).scalar() or 0 logger.info(f"找到 {total_users} 个需要更新的用户") - # 遍历每个用户并更新数据 - for end_user_id in user_ids: + # 遍历每个用户并更新数据(分批次,避免一次性加载所有ID) + for end_user_id in repo.get_all_user_ids(batch_size=100): logger.info(f"开始处理用户: {end_user_id}") user_start_time = time.time() From a3f05220d3bd49ab318d12ec48ef2a47cba4bd4d Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 16:16:16 +0800 Subject: [PATCH 27/83] [changes] Test the scheduled task --- api/app/controllers/emotion_controller.py | 88 +++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 0a8b5fc8..8cfc5014 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -208,55 +208,55 @@ async def get_emotion_health( -@router.post("/check-data", response_model=ApiResponse) -async def check_emotion_data_exists( - request: EmotionSuggestionsRequest, - db: Session = Depends(get_db), - current_user: User = Depends(get_current_user), -): - """检查用户情绪建议数据是否存在 +# @router.post("/check-data", response_model=ApiResponse) +# async def check_emotion_data_exists( +# request: EmotionSuggestionsRequest, +# db: Session = Depends(get_db), +# current_user: User = Depends(get_current_user), +# ): +# """检查用户情绪建议数据是否存在 - Args: - request: 包含 end_user_id - db: 数据库会话 - current_user: 当前用户 +# Args: +# request: 包含 end_user_id +# db: 数据库会话 +# current_user: 当前用户 - Returns: - 数据存在状态 - """ - try: - api_logger.info( - f"检查用户情绪建议数据是否存在: {request.end_user_id}", - extra={"end_user_id": request.end_user_id} - ) +# Returns: +# 数据存在状态 +# """ +# try: +# api_logger.info( +# f"检查用户情绪建议数据是否存在: {request.end_user_id}", +# extra={"end_user_id": request.end_user_id} +# ) - # 从数据库获取建议 - data = await emotion_service.get_cached_suggestions( - end_user_id=request.end_user_id, - db=db - ) +# # 从数据库获取建议 +# data = await emotion_service.get_cached_suggestions( +# end_user_id=request.end_user_id, +# db=db +# ) - if data is None: - api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") - return fail( - BizCode.NOT_FOUND, - "情绪建议数据不存在,请点击右上角刷新进行初始化", - {"exists": False} - ) +# if data is None: +# api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据不存在") +# return fail( +# BizCode.NOT_FOUND, +# "情绪建议数据不存在,请点击右上角刷新进行初始化", +# {"exists": False} +# ) - api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") - return success(data={"exists": True}, msg="情绪建议数据已存在") +# api_logger.info(f"用户 {request.end_user_id} 的情绪建议数据存在") +# return success(data={"exists": True}, msg="情绪建议数据已存在") - except Exception as e: - api_logger.error( - f"检查情绪建议数据失败: {str(e)}", - extra={"end_user_id": request.end_user_id}, - exc_info=True - ) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"检查情绪建议数据失败: {str(e)}" - ) +# except Exception as e: +# api_logger.error( +# f"检查情绪建议数据失败: {str(e)}", +# extra={"end_user_id": request.end_user_id}, +# exc_info=True +# ) +# raise HTTPException( +# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, +# detail=f"检查情绪建议数据失败: {str(e)}" +# ) @router.post("/suggestions", response_model=ApiResponse) @@ -383,4 +383,4 @@ async def generate_emotion_suggestions( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"生成个性化建议失败: {str(e)}" - ) + ) \ No newline at end of file From 941527e7ee82fcbd80b1eb012e70aaa057e68589 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Tue, 3 Mar 2026 16:47:50 +0800 Subject: [PATCH 28/83] [changes] Modify the pop-up window for emotional suggestions at the backend --- api/app/controllers/emotion_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index 8cfc5014..ea7b719f 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -295,8 +295,8 @@ async def get_emotion_suggestions( f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - return success( - data={"exists": False}, + return fail( + code=404, msg="情绪建议数据不存在,请点击右上角刷新进行初始化" ) From a726a81224b7f5dca86ee6471c8741d74525c4dc Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 13:39:21 +0800 Subject: [PATCH 29/83] [changes]Specifies the time zone divisions --- .../repositories/implicit_emotions_storage_repository.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/app/repositories/implicit_emotions_storage_repository.py b/api/app/repositories/implicit_emotions_storage_repository.py index 1d11f89e..97405ab6 100644 --- a/api/app/repositories/implicit_emotions_storage_repository.py +++ b/api/app/repositories/implicit_emotions_storage_repository.py @@ -5,7 +5,7 @@ Implicit Emotions Storage Repository 事务由调用方控制,仓储层只使用 flush/refresh """ import logging -from datetime import datetime, date +from datetime import datetime, date, timezone, timedelta from typing import Optional, Generator from sqlalchemy.orm import Session from sqlalchemy import select, not_, exists @@ -125,7 +125,10 @@ class ImplicitEmotionsStorageRepository: 用户ID字符串 """ from sqlalchemy import cast, String as SAString - today_start = datetime.combine(date.today(), datetime.min.time()) + CST = timezone(timedelta(hours=8)) + now_cst = datetime.now(CST) + today_start = now_cst.replace(hour=0, minute=0, second=0, microsecond=0).astimezone(timezone.utc).replace(tzinfo=None) + tomorrow_start = today_start + timedelta(days=1) offset = 0 while True: try: @@ -133,6 +136,7 @@ class ImplicitEmotionsStorageRepository: select(EndUser.id) .where( EndUser.created_at >= today_start, + EndUser.created_at < tomorrow_start, not_( exists( select(ImplicitEmotionsStorage.end_user_id).where( From d4c4160215f9bdf35f941d7c3242dbed9795fa1c Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Wed, 4 Mar 2026 15:28:17 +0800 Subject: [PATCH 30/83] =?UTF-8?q?=E3=80=90ADD]Knowledge=20base=20retrieval?= =?UTF-8?q?=20supports=20file=20set=20retrieval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/controllers/chunk_controller.py | 8 ++++---- api/app/schemas/chunk_schema.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/app/controllers/chunk_controller.py b/api/app/controllers/chunk_controller.py index 620d8a1a..988aa706 100644 --- a/api/app/controllers/chunk_controller.py +++ b/api/app/controllers/chunk_controller.py @@ -441,14 +441,14 @@ async def retrieve_chunks( # 1 participle search, 2 semantic search, 3 hybrid search match retrieve_data.retrieve_type: case chunk_schema.RetrieveType.PARTICIPLE: - rs = vector_service.search_by_full_text(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.similarity_threshold) + rs = vector_service.search_by_full_text(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.similarity_threshold, file_names_filter=retrieve_data.file_names_filter) return success(data=rs, msg="retrieval successful") case chunk_schema.RetrieveType.SEMANTIC: - rs = vector_service.search_by_vector(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.vector_similarity_weight) + rs = vector_service.search_by_vector(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.vector_similarity_weight, file_names_filter=retrieve_data.file_names_filter) return success(data=rs, msg="retrieval successful") case _: - rs1 = vector_service.search_by_vector(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.vector_similarity_weight) - rs2 = vector_service.search_by_full_text(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.similarity_threshold) + rs1 = vector_service.search_by_vector(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.vector_similarity_weight, file_names_filter=retrieve_data.file_names_filter) + rs2 = vector_service.search_by_full_text(query=retrieve_data.query, top_k=retrieve_data.top_k, indices=indices, score_threshold=retrieve_data.similarity_threshold, file_names_filter=retrieve_data.file_names_filter) # Efficient deduplication seen_ids = set() unique_rs = [] diff --git a/api/app/schemas/chunk_schema.py b/api/app/schemas/chunk_schema.py index cef9b9cb..ce8f70f2 100644 --- a/api/app/schemas/chunk_schema.py +++ b/api/app/schemas/chunk_schema.py @@ -46,6 +46,7 @@ class ChunkUpdate(BaseModel): class ChunkRetrieve(BaseModel): query: str kb_ids: list[uuid.UUID] + file_names_filter: list[str] | None = Field(None) similarity_threshold: float | None = Field(None) vector_similarity_weight: float | None = Field(None) top_k: int | None = Field(None) From 778bc4bd7008f7a1ea3b0aa6a8c6b822b9240603 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Wed, 4 Mar 2026 15:58:49 +0800 Subject: [PATCH 31/83] fix(workflow): fix incorrect fields in streaming API output --- api/app/controllers/service/app_api_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/app/controllers/service/app_api_controller.py b/api/app/controllers/service/app_api_controller.py index 61a919b1..64143f57 100644 --- a/api/app/controllers/service/app_api_controller.py +++ b/api/app/controllers/service/app_api_controller.py @@ -249,6 +249,7 @@ async def chat( app_id=app.id, workspace_id=workspace_id, release_id=app.current_release.id, + public=True ): event_type = event.get("event", "message") event_data = event.get("data", {}) From 5929072b76b7bd3b4daefa5ed14f39cc32bf7071 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 16:24:00 +0800 Subject: [PATCH 32/83] [changes] Emotional suggestions should not return error messages. --- api/app/controllers/emotion_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/app/controllers/emotion_controller.py b/api/app/controllers/emotion_controller.py index ea7b719f..8cfc5014 100644 --- a/api/app/controllers/emotion_controller.py +++ b/api/app/controllers/emotion_controller.py @@ -295,8 +295,8 @@ async def get_emotion_suggestions( f"用户 {request.end_user_id} 的建议数据不存在", extra={"end_user_id": request.end_user_id} ) - return fail( - code=404, + return success( + data={"exists": False}, msg="情绪建议数据不存在,请点击右上角刷新进行初始化" ) From 34de0bb9c5b7d11b6012a0b958d1f16185cef605 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 16:28:28 +0800 Subject: [PATCH 33/83] fix(web): memory use modal replace --- web/src/i18n/en.ts | 3 ++- web/src/i18n/zh.ts | 3 ++- .../components/Suggestions.tsx | 23 +++++++++++++--- .../UserMemoryDetail/pages/ImplicitDetail.tsx | 27 ++++++++++++++++--- .../pages/StatementDetail.tsx | 18 +++++++++++-- .../views/UserMemoryDetail/pages/index.tsx | 24 ++++++++++++++--- 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index b17ad291..352fc4b6 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2276,6 +2276,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re suggestions: 'Personalized Suggestions', suggestionLoading: 'Your personalized suggestions are being generated', item: 'item', + noData: 'Emotion suggestion data does not exist, please click the refresh button to initialize', }, reflectionEngine: { reflectionEngineConfig: 'Reflection Engine Configuration', @@ -2523,7 +2524,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re supporting_evidence: 'Preference Source', specific_examples: 'Source', wordEmpty: 'Click on a node in the left chart to view preference details', - noData: 'Portrait data does not exist, please click the refresh button in the top right corner to initialize', + noData: 'Portrait data does not exist, please click the refresh button to initialize', }, shortTermDetail: { title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 181173ff..92f0710c 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2272,6 +2272,7 @@ export const zh = { suggestions: '个性化建议', suggestionLoading: '您的个性化建议正在生成中', item: '个', + noData: '情绪建议数据不存在,请点击刷新按钮进行初始化', }, reflectionEngine: { reflectionEngineConfig: '反思引擎配置', @@ -2519,7 +2520,7 @@ export const zh = { supporting_evidence: '偏好来源', specific_examples: '来源', wordEmpty: '点击左侧图表中的节点查看偏好详情', - noData: '画像数据不存在,请点击右上角刷新进行初始化', + noData: '画像数据不存在,请点击刷新按钮进行初始化', }, shortTermDetail: { title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。', diff --git a/web/src/views/UserMemoryDetail/components/Suggestions.tsx b/web/src/views/UserMemoryDetail/components/Suggestions.tsx index 55bfbf14..c67c0d80 100644 --- a/web/src/views/UserMemoryDetail/components/Suggestions.tsx +++ b/web/src/views/UserMemoryDetail/components/Suggestions.tsx @@ -1,12 +1,13 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 18:31:50 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 18:31:50 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 16:22:03 */ import { useEffect, useState, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' +import { App } from 'antd' import Empty from '@/components/Empty' import RbCard from '@/components/RbCard/Card' @@ -20,6 +21,7 @@ import RbAlert from '@/components/RbAlert' * @property {Array} suggestions - List of suggestions with actionable steps */ interface Suggestions { + exists?: boolean; health_summary: string; suggestions: Array<{ type: string; @@ -35,9 +37,10 @@ interface Suggestions { * Displays emotional health suggestions with actionable steps * Shows health summary and prioritized recommendations */ -const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => { +const Suggestions = forwardRef<{ handleRefresh: () => void; }, { refresh: () => void; }>(({ refresh }, ref) => { const { t } = useTranslation() const { id } = useParams() + const { modal } = App.useApp() const [loading, setLoading] = useState(false) const [suggestions, setSuggestions] = useState(null) @@ -52,7 +55,19 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => setLoading(true) getEmotionSuggestions(id) .then((res) => { - setSuggestions(res as Suggestions) + const response = res as Suggestions + if (!response.exists && (!response.suggestions || !response.suggestions?.length)) { + modal.confirm({ + title: t('statementDetail.noData'), + okText: t('common.refresh'), + cancelText: t('common.cancel'), + onOk: () => { + refresh() + } + }) + } else { + setSuggestions(res as Suggestions) + } }) .finally(() => { setLoading(false) diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx index 351e5ed1..aa6f40c7 100644 --- a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-01-08 19:46:02 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 16:26:55 + */ import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Row, Col, App } from 'antd' @@ -12,25 +18,40 @@ import { implicitCheckData, } from '@/api/memory' -const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => { +/** + * ImplicitDetail Component - Displays user's implicit memory profile + * Shows unconscious preferences, personality traits, interests and habits + */ +const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }, { refresh: () => void; }>(({ + refresh +}, ref) => { const { t } = useTranslation() const { id } = useParams() - const { message } = App.useApp() + const { modal } = App.useApp() const preferencesRef = useRef<{ handleRefresh: () => void; }>(null) const portraitRef = useRef<{ handleRefresh: () => void; }>(null) const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null) const habitsRef = useRef<{ handleRefresh: () => void; }>(null) + // Check if implicit data exists, prompt user to initialize if not useEffect(() => { if (!id) return implicitCheckData(id) .then(res => { if (!(res as { exists: boolean }).exists) { - message.warning(t('implicitDetail.noData')) + modal.confirm({ + title: t('implicitDetail.noData'), + okText: t('common.refresh'), + cancelText: t('common.cancel'), + onOk: () => { + refresh() + } + }) } }) }, [id]) + // Refresh all implicit memory components by regenerating profile const handleRefresh = () => { if (!id) { return Promise.resolve() diff --git a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx index 72d35c60..cddf95ad 100644 --- a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-19 16:54:52 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 16:28:00 + */ import { forwardRef, useImperativeHandle, useRef } from 'react' import { Row, Col, Space } from 'antd'; import { useParams } from 'react-router-dom' @@ -9,9 +15,17 @@ import Suggestions from '../components/Suggestions' import { generateSuggestions } from '@/api/memory' -const StatementDetail = forwardRef((_props, ref) => { +/** + * StatementDetail - Displays emotional memory analysis for a user + * Shows word cloud, emotion tags, health index, and personalized suggestions + */ +const StatementDetail = forwardRef<{ handleRefresh: () => void },{ refresh: () => void; }>(({ + refresh +}, ref) => { const { id } = useParams() const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null) + + // Regenerate suggestions and refresh the Suggestions child component const handleRefresh = () => { if (!id) { return Promise.resolve() @@ -41,7 +55,7 @@ const StatementDetail = forwardRef((_props, ref) => { - + ) diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx index c5dea163..71cada89 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -1,8 +1,13 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-01-07 20:37:34 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 16:27:14 + */ import { type FC, useEffect, useState, useMemo, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Dropdown, Button } from 'antd' -import { LoadingOutlined } from '@ant-design/icons'; import PageHeader from '../components/PageHeader' import StatementDetail from './StatementDetail' @@ -19,11 +24,16 @@ import { import refreshIcon from '@/assets/images/refresh_hover.svg' import GraphDetail from './GraphDetail' +/** + * Detail page for user memory - renders different memory type views + * based on the `type` route param + */ const Detail: FC = () => { const { t } = useTranslation() const { id, type } = useParams() const navigate = useNavigate() const [name, setName] = useState('') + // Refs for child components that support imperative refresh const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null) const statementDetailRef = useRef<{ handleRefresh: () => void }>(null) const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null) @@ -33,6 +43,7 @@ const Detail: FC = () => { getData() }, [id]) + // Fetch end user profile to display the user's name in the header const getData = () => { if (!id) return getEndUserProfile(id).then((res) => { @@ -40,15 +51,21 @@ const Detail: FC = () => { setName(response.other_name || response.id) }) } + + // Build dropdown menu items for switching between memory types const items = useMemo(() => { return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGET_MEMORY'] .map(key => ({ key, label: t(`userMemory.${key}`) })) }, [t]) + + // Navigate to the selected memory type detail page const onClick = ({ key }: { key: string }) => { navigate(`/user-memory/detail/${id}/${key}`, { replace: true }) } const [loading, setLoading] = useState(false) + + // Trigger refresh on the active memory type's child component const handleRefresh = () => { setLoading(true) let response: any = null @@ -64,6 +81,7 @@ const Detail: FC = () => { break } + // If the child returns a Promise, wait for it before clearing loading state if (response instanceof Promise) { response.finally(() => { setLoading(false) @@ -99,9 +117,9 @@ const Detail: FC = () => { } />
- {type === 'EMOTIONAL_MEMORY' && } + {type === 'EMOTIONAL_MEMORY' && } {type === 'FORGET_MEMORY' && } - {type === 'IMPLICIT_MEMORY' && } + {type === 'IMPLICIT_MEMORY' && } {type === 'SHORT_TERM_MEMORY' && } {type === 'PERCEPTUAL_MEMORY' && } {type === 'EPISODIC_MEMORY' && } From 440e8acd990547baaf7e6ba70c5471693b312a14 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 16:42:15 +0800 Subject: [PATCH 34/83] feat(web): mcp tool add form rules --- web/src/i18n/en.ts | 4 +++- web/src/i18n/zh.ts | 4 +++- .../ToolManagement/components/McpServiceModal.tsx | 14 ++++++++++++-- .../components/RequestHeaderModal.tsx | 13 +++++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 94dd67cc..a80a0aa4 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1942,7 +1942,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re path: 'Path', viewDetail: 'View Details', textLink: 'Test Connection', - noResult: 'Processing results will be displayed here' + noResult: 'Processing results will be displayed here', + serverUrlInvalid: 'Must start with http:// or https://, and cannot have leading or trailing spaces', + requestHeaderKeyInvalid: 'Only English letters, numbers, hyphens (-), and underscores (_) are allowed, and cannot start or end with a hyphen or underscore', }, workflow: { coreNode: 'Core Nodes', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 49632789..1302fdad 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1939,7 +1939,9 @@ export const zh = { path: '路径', viewDetail: '查看详情', textLink: '测试连接', - noResult: '处理结果将显示在这里' + noResult: '处理结果将显示在这里', + serverUrlInvalid: '必须以 http:// 或 https:// 开头,且不能有前后空格', + requestHeaderKeyInvalid: '只支持英文、数字、连字符(-)、下划线(_),不能以连字符或下划线开头结尾', }, workflow: { coreNode: '核心节点', diff --git a/web/src/views/ToolManagement/components/McpServiceModal.tsx b/web/src/views/ToolManagement/components/McpServiceModal.tsx index a104c2d6..bd97b876 100644 --- a/web/src/views/ToolManagement/components/McpServiceModal.tsx +++ b/web/src/views/ToolManagement/components/McpServiceModal.tsx @@ -9,6 +9,7 @@ import RequestHeaderModal from './RequestHeaderModal'; import Table from '@/components/Table'; import { addTool, updateTool, testConnection } from '@/api/tools' import type { McpServiceModalRef } from '../types' +import { stringRegExp } from '@/utils/validator'; const FormItem = Form.Item; @@ -168,14 +169,22 @@ const McpServiceModal = forwardRef(({ name={['config', "server_url"]} label={t('tool.serviceEndpoint')} extra={t('tool.serviceEndpointExtra')} - rules={[{ required: true, message: t('common.pleaseEnter') }]} + rules={[ + { required: true, message: t('common.pleaseEnter') }, + { max: 500 }, + { pattern: /^https?:\/\/\S+$/, message: t('tool.serverUrlInvalid') }, + ]} > @@ -201,6 +210,7 @@ const McpServiceModal = forwardRef(({ diff --git a/web/src/views/ToolManagement/components/RequestHeaderModal.tsx b/web/src/views/ToolManagement/components/RequestHeaderModal.tsx index 5e20120d..1f2bdff3 100644 --- a/web/src/views/ToolManagement/components/RequestHeaderModal.tsx +++ b/web/src/views/ToolManagement/components/RequestHeaderModal.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { RequestHeader, RequestHeaderModalRef } from './McpServiceModal' import RbModal from '@/components/RbModal' +import { stringRegExp } from '@/utils/validator'; const FormItem = Form.Item; @@ -82,7 +83,11 @@ const RequestHeaderModal = forwardRef @@ -90,7 +95,11 @@ const RequestHeaderModal = forwardRef From 4ee198813a360787b29356219bd49f33b3af0e68 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 16:46:25 +0800 Subject: [PATCH 35/83] feat(web): custom tool add form rules --- .../views/ToolManagement/components/CustomToolModal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/views/ToolManagement/components/CustomToolModal.tsx b/web/src/views/ToolManagement/components/CustomToolModal.tsx index e3203a74..0ea8aca9 100644 --- a/web/src/views/ToolManagement/components/CustomToolModal.tsx +++ b/web/src/views/ToolManagement/components/CustomToolModal.tsx @@ -6,6 +6,7 @@ import type { CustomToolItem, CustomToolModalRef, ToolItem } from '../types' import RbModal from '@/components/RbModal'; import { parseSchema, addTool, updateTool } from '@/api/tools'; import Table from '@/components/Table'; +import { stringRegExp } from '@/utils/validator'; const FormItem = Form.Item; interface CustomToolModalProps { @@ -134,7 +135,11 @@ const CustomToolModal = forwardRef(({ From f5eda38dc99edbf71affe3a56b04d40bc830b0a6 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 17:04:25 +0800 Subject: [PATCH 36/83] feat(web): ontology extract add form rules --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/views/Ontology/components/OntologyClassExtractModal.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index a80a0aa4..6410a2e9 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -456,6 +456,7 @@ export const en = { logoTip: `Supported image formats: JPG, PNG \n Suggested size: square ratio \n Maximum size: ≤ 2MB`, imageSquareRequired: 'Please upload a square image', nameInvalid: 'Name cannot start or end with a space', + notAllSpaces: 'Cannot be all spaces', }, model: { searchPlaceholder: 'search model…', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 1302fdad..889154f4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1032,6 +1032,7 @@ export const zh = { logoTip: `支持图片格式(JPG、PNG)\n 尺寸:正方形比例 \n 文件大小限制:≤ 2MB`, imageSquareRequired: '请上传正方形比例图片', nameInvalid: '不能是空格开头或结尾', + notAllSpaces: '不能是纯空格', }, model: { searchPlaceholder: '搜索模型…', diff --git a/web/src/views/Ontology/components/OntologyClassExtractModal.tsx b/web/src/views/Ontology/components/OntologyClassExtractModal.tsx index 2fd305c6..8d3e1a91 100644 --- a/web/src/views/Ontology/components/OntologyClassExtractModal.tsx +++ b/web/src/views/Ontology/components/OntologyClassExtractModal.tsx @@ -185,6 +185,7 @@ const OntologyClassExtractModal = forwardRef From 53dbe2f436bc1f71dde7f8ff46f9dfab68f861b3 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 17:26:30 +0800 Subject: [PATCH 37/83] [fix] Fix the external write memory API --- api/app/controllers/service/memory_api_controller.py | 2 +- api/app/services/memory_api_service.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/app/controllers/service/memory_api_controller.py b/api/app/controllers/service/memory_api_controller.py index accd749e..34489e8a 100644 --- a/api/app/controllers/service/memory_api_controller.py +++ b/api/app/controllers/service/memory_api_controller.py @@ -39,7 +39,7 @@ async def write_memory_api_service( Stores memory content for the specified end user using the Memory API Service. """ - logger.info(f"Memory write request - end_user_id: {payload.end_user_id}, tenant_id: {api_key_auth.tenant_id}") + logger.info(f"Memory write request - end_user_id: {payload.end_user_id}, workspace_id: {api_key_auth.workspace_id}") memory_api_service = MemoryAPIService(db) diff --git a/api/app/services/memory_api_service.py b/api/app/services/memory_api_service.py index a8c39a5a..ad0a8164 100644 --- a/api/app/services/memory_api_service.py +++ b/api/app/services/memory_api_service.py @@ -140,9 +140,11 @@ class MemoryAPIService: try: # Delegate to MemoryAgentService + # Convert string message to list[dict] format expected by MemoryAgentService + messages = message if isinstance(message, list) else [{"role": "user", "content": message}] result = await MemoryAgentService().write_memory( end_user_id=end_user_id, - messages=message, + messages=messages, config_id=config_id, db=self.db, storage_type=storage_type, @@ -151,8 +153,13 @@ class MemoryAPIService: logger.info(f"Memory write successful for end_user: {end_user_id}") + # result may be a string "success" or a dict with a "status" key + if isinstance(result, dict): + status = result.get("status", "success") + else: + status = result if isinstance(result, str) else "success" return { - "status": "success" if result == "success" else result, + "status": status, "end_user_id": end_user_id } From efe3865aa44fdb8ed8ad469a5b714565a83d512e Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 17:26:30 +0800 Subject: [PATCH 38/83] [fix] Fix the external write memory API --- api/app/controllers/service/memory_api_controller.py | 2 +- api/app/services/memory_api_service.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/app/controllers/service/memory_api_controller.py b/api/app/controllers/service/memory_api_controller.py index accd749e..34489e8a 100644 --- a/api/app/controllers/service/memory_api_controller.py +++ b/api/app/controllers/service/memory_api_controller.py @@ -39,7 +39,7 @@ async def write_memory_api_service( Stores memory content for the specified end user using the Memory API Service. """ - logger.info(f"Memory write request - end_user_id: {payload.end_user_id}, tenant_id: {api_key_auth.tenant_id}") + logger.info(f"Memory write request - end_user_id: {payload.end_user_id}, workspace_id: {api_key_auth.workspace_id}") memory_api_service = MemoryAPIService(db) diff --git a/api/app/services/memory_api_service.py b/api/app/services/memory_api_service.py index a8c39a5a..ad0a8164 100644 --- a/api/app/services/memory_api_service.py +++ b/api/app/services/memory_api_service.py @@ -140,9 +140,11 @@ class MemoryAPIService: try: # Delegate to MemoryAgentService + # Convert string message to list[dict] format expected by MemoryAgentService + messages = message if isinstance(message, list) else [{"role": "user", "content": message}] result = await MemoryAgentService().write_memory( end_user_id=end_user_id, - messages=message, + messages=messages, config_id=config_id, db=self.db, storage_type=storage_type, @@ -151,8 +153,13 @@ class MemoryAPIService: logger.info(f"Memory write successful for end_user: {end_user_id}") + # result may be a string "success" or a dict with a "status" key + if isinstance(result, dict): + status = result.get("status", "success") + else: + status = result if isinstance(result, str) else "success" return { - "status": "success" if result == "success" else result, + "status": status, "end_user_id": end_user_id } From ba36ccb21fd42c678f9b3748d7525dd747787c0e Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 17:46:13 +0800 Subject: [PATCH 39/83] [changes] Hide the user knowledge base and unify the display of memory capacity --- .../memory_dashboard_controller.py | 4 +- api/app/repositories/knowledge_repository.py | 43 +++++++++++++++++ api/app/services/memory_dashboard_service.py | 46 +++++++++++++++++-- api/app/tasks.py | 13 ++++-- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/api/app/controllers/memory_dashboard_controller.py b/api/app/controllers/memory_dashboard_controller.py index 475d184e..1b5b45fb 100644 --- a/api/app/controllers/memory_dashboard_controller.py +++ b/api/app/controllers/memory_dashboard_controller.py @@ -606,8 +606,8 @@ async def dashboard_data( # 获取RAG相关数据 try: - # total_memory: 使用 total_chunk(总chunk数) - total_chunk = memory_dashboard_service.get_rag_total_chunk(db, current_user) + # total_memory: 只统计用户知识库(permission_id='Memory')的chunk数 + total_chunk = memory_dashboard_service.get_rag_user_kb_total_chunk(db, current_user) rag_data["total_memory"] = total_chunk # total_app: 统计当前空间下的所有app数量 diff --git a/api/app/repositories/knowledge_repository.py b/api/app/repositories/knowledge_repository.py index 681d1c10..e3832214 100644 --- a/api/app/repositories/knowledge_repository.py +++ b/api/app/repositories/knowledge_repository.py @@ -211,3 +211,46 @@ def get_total_kb_count_by_workspace(db: Session, workspace_id: uuid.UUID) -> int except Exception as e: db_logger.error(f"Failed to query total knowledge base count: workspace_id={workspace_id} - {str(e)}") raise + + +def get_user_kb_chunk_num_by_workspace(db: Session, workspace_id: uuid.UUID) -> int: + """ + 根据workspace_id查询knowledges表中permission_id='Memory'(用户知识库)的chunk_num总和 + """ + db_logger.debug(f"Query user KB chunk_num by workspace_id: workspace_id={workspace_id}") + + try: + from sqlalchemy import func + result = db.query(func.sum(Knowledge.chunk_num)).filter( + Knowledge.workspace_id == workspace_id, + Knowledge.status == 1, + Knowledge.permission_id == "Memory" + ).scalar() + + total = result if result is not None else 0 + db_logger.info(f"User KB chunk_num query successful: workspace_id={workspace_id}, total={total}") + return total + except Exception as e: + db_logger.error(f"Failed to query user KB chunk_num: workspace_id={workspace_id} - {str(e)}") + raise + + +def get_non_user_kb_count_by_workspace(db: Session, workspace_id: uuid.UUID) -> int: + """ + 根据workspace_id查询knowledges表中排除用户知识库(permission_id!='Memory')的数量 + """ + db_logger.debug(f"Query non-user KB count by workspace_id: workspace_id={workspace_id}") + + try: + count = db.query(Knowledge).filter( + Knowledge.workspace_id == workspace_id, + Knowledge.status == 1, + Knowledge.permission_id != "Memory" + ).count() + + db_logger.info(f"Non-user KB count query successful: workspace_id={workspace_id}, count={count}") + return count + except Exception as e: + db_logger.error(f"Failed to query non-user KB count: workspace_id={workspace_id} - {str(e)}") + raise + diff --git a/api/app/services/memory_dashboard_service.py b/api/app/services/memory_dashboard_service.py index 8d6071cc..22752805 100644 --- a/api/app/services/memory_dashboard_service.py +++ b/api/app/services/memory_dashboard_service.py @@ -390,19 +390,59 @@ def get_rag_total_kb( current_user: User ) -> int: """ - 根据当前用户所在的workspace_id查询konwledges表所有不同id的数量 + 根据当前用户所在的workspace_id查询konwledges表中排除用户知识库(permission_id!='Memory')的数量 """ workspace_id = current_user.current_workspace_id - business_logger.info(f"获取RAG总知识库数: workspace_id={workspace_id}, 操作者: {current_user.username}") + business_logger.info(f"获取RAG总知识库数(排除用户知识库): workspace_id={workspace_id}, 操作者: {current_user.username}") try: - total_kb = knowledge_repository.get_total_kb_count_by_workspace(db, workspace_id) + total_kb = knowledge_repository.get_non_user_kb_count_by_workspace(db, workspace_id) business_logger.info(f"成功获取RAG总知识库数: {total_kb}") return total_kb except Exception as e: business_logger.error(f"获取RAG总知识库数失败: workspace_id={workspace_id} - {str(e)}") raise + +def get_rag_user_kb_total_chunk( + db: Session, + current_user: User +) -> int: + """ + 根据当前用户所在的workspace_id,从documents表统计所有用户知识库的chunk总数。 + 与 /end_users 接口保持同源:查询 file_name 匹配 end_user_id.txt 的文档 chunk_num 之和。 + """ + workspace_id = current_user.current_workspace_id + business_logger.info(f"获取用户知识库总chunk数(documents表): workspace_id={workspace_id}, 操作者: {current_user.username}") + + try: + from app.models.document_model import Document + from app.models.end_user_model import EndUser + from app.models.app_model import App + from sqlalchemy import func + + # 通过 App 关联取该 workspace 下所有 end_user_id + end_user_ids = [ + str(u.id) for u in db.query(EndUser.id) + .join(App, EndUser.app_id == App.id) + .filter(App.workspace_id == workspace_id) + .all() + ] + if not end_user_ids: + return 0 + + file_names = [f"{uid}.txt" for uid in end_user_ids] + result = db.query(func.sum(Document.chunk_num)).filter( + Document.file_name.in_(file_names) + ).scalar() + + total_chunk = int(result or 0) + business_logger.info(f"成功获取用户知识库总chunk数: {total_chunk}") + return total_chunk + except Exception as e: + business_logger.error(f"获取用户知识库总chunk数失败: workspace_id={workspace_id} - {str(e)}") + raise + def get_current_user_total_chunk( end_user_id: str, db: Session, diff --git a/api/app/tasks.py b/api/app/tasks.py index 299d188b..671a03f4 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -62,7 +62,7 @@ def process_item(item: dict): @celery_app.task(name="app.core.rag.tasks.parse_document") -def parse_document(file_path: str, document_id: uuid.UUID): +def parse_document(file_path: str, document_id: str): """ Document parsing, vectorization, and storage """ @@ -74,6 +74,9 @@ def parse_document(file_path: str, document_id: uuid.UUID): db = next(get_db()) # Manually call the generator db_document = None db_knowledge = None + # 确保 document_id 是 UUID 对象 + if not isinstance(document_id, uuid.UUID): + document_id = uuid.UUID(str(document_id)) progress_msg = f"{datetime.now().strftime('%H:%M:%S')} Task has been received.\n" try: db_document = db.query(Document).filter(Document.id == document_id).first() @@ -282,11 +285,13 @@ def parse_document(file_path: str, document_id: uuid.UUID): result = f"parse document '{db_document.file_name}' processed successfully." return result except Exception as e: - if 'db_document' in locals(): - db_document.progress_msg += f"Failed to vectorize and import the parsed document:{str(e)}\n" + if db_document is not None: + db_document.progress_msg = (db_document.progress_msg or "") + f"Failed to vectorize and import the parsed document: {str(e)}\n" db_document.run = 0 db.commit() - result = f"parse document '{db_document.file_name}' failed." + result = f"parse document '{db_document.file_name}' failed." + else: + result = f"parse document '{document_id}' failed: document not found in DB. error={str(e)}" return result finally: db.close() From 850d9ee70b098b18d604c6b763e167a855c76fe5 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 17:46:13 +0800 Subject: [PATCH 40/83] [changes] Hide the user knowledge base and unify the display of memory capacity --- .../memory_dashboard_controller.py | 4 +- api/app/repositories/knowledge_repository.py | 43 +++++++++++++++++ api/app/services/memory_dashboard_service.py | 46 +++++++++++++++++-- api/app/tasks.py | 13 ++++-- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/api/app/controllers/memory_dashboard_controller.py b/api/app/controllers/memory_dashboard_controller.py index 475d184e..1b5b45fb 100644 --- a/api/app/controllers/memory_dashboard_controller.py +++ b/api/app/controllers/memory_dashboard_controller.py @@ -606,8 +606,8 @@ async def dashboard_data( # 获取RAG相关数据 try: - # total_memory: 使用 total_chunk(总chunk数) - total_chunk = memory_dashboard_service.get_rag_total_chunk(db, current_user) + # total_memory: 只统计用户知识库(permission_id='Memory')的chunk数 + total_chunk = memory_dashboard_service.get_rag_user_kb_total_chunk(db, current_user) rag_data["total_memory"] = total_chunk # total_app: 统计当前空间下的所有app数量 diff --git a/api/app/repositories/knowledge_repository.py b/api/app/repositories/knowledge_repository.py index 681d1c10..e3832214 100644 --- a/api/app/repositories/knowledge_repository.py +++ b/api/app/repositories/knowledge_repository.py @@ -211,3 +211,46 @@ def get_total_kb_count_by_workspace(db: Session, workspace_id: uuid.UUID) -> int except Exception as e: db_logger.error(f"Failed to query total knowledge base count: workspace_id={workspace_id} - {str(e)}") raise + + +def get_user_kb_chunk_num_by_workspace(db: Session, workspace_id: uuid.UUID) -> int: + """ + 根据workspace_id查询knowledges表中permission_id='Memory'(用户知识库)的chunk_num总和 + """ + db_logger.debug(f"Query user KB chunk_num by workspace_id: workspace_id={workspace_id}") + + try: + from sqlalchemy import func + result = db.query(func.sum(Knowledge.chunk_num)).filter( + Knowledge.workspace_id == workspace_id, + Knowledge.status == 1, + Knowledge.permission_id == "Memory" + ).scalar() + + total = result if result is not None else 0 + db_logger.info(f"User KB chunk_num query successful: workspace_id={workspace_id}, total={total}") + return total + except Exception as e: + db_logger.error(f"Failed to query user KB chunk_num: workspace_id={workspace_id} - {str(e)}") + raise + + +def get_non_user_kb_count_by_workspace(db: Session, workspace_id: uuid.UUID) -> int: + """ + 根据workspace_id查询knowledges表中排除用户知识库(permission_id!='Memory')的数量 + """ + db_logger.debug(f"Query non-user KB count by workspace_id: workspace_id={workspace_id}") + + try: + count = db.query(Knowledge).filter( + Knowledge.workspace_id == workspace_id, + Knowledge.status == 1, + Knowledge.permission_id != "Memory" + ).count() + + db_logger.info(f"Non-user KB count query successful: workspace_id={workspace_id}, count={count}") + return count + except Exception as e: + db_logger.error(f"Failed to query non-user KB count: workspace_id={workspace_id} - {str(e)}") + raise + diff --git a/api/app/services/memory_dashboard_service.py b/api/app/services/memory_dashboard_service.py index 8d6071cc..22752805 100644 --- a/api/app/services/memory_dashboard_service.py +++ b/api/app/services/memory_dashboard_service.py @@ -390,19 +390,59 @@ def get_rag_total_kb( current_user: User ) -> int: """ - 根据当前用户所在的workspace_id查询konwledges表所有不同id的数量 + 根据当前用户所在的workspace_id查询konwledges表中排除用户知识库(permission_id!='Memory')的数量 """ workspace_id = current_user.current_workspace_id - business_logger.info(f"获取RAG总知识库数: workspace_id={workspace_id}, 操作者: {current_user.username}") + business_logger.info(f"获取RAG总知识库数(排除用户知识库): workspace_id={workspace_id}, 操作者: {current_user.username}") try: - total_kb = knowledge_repository.get_total_kb_count_by_workspace(db, workspace_id) + total_kb = knowledge_repository.get_non_user_kb_count_by_workspace(db, workspace_id) business_logger.info(f"成功获取RAG总知识库数: {total_kb}") return total_kb except Exception as e: business_logger.error(f"获取RAG总知识库数失败: workspace_id={workspace_id} - {str(e)}") raise + +def get_rag_user_kb_total_chunk( + db: Session, + current_user: User +) -> int: + """ + 根据当前用户所在的workspace_id,从documents表统计所有用户知识库的chunk总数。 + 与 /end_users 接口保持同源:查询 file_name 匹配 end_user_id.txt 的文档 chunk_num 之和。 + """ + workspace_id = current_user.current_workspace_id + business_logger.info(f"获取用户知识库总chunk数(documents表): workspace_id={workspace_id}, 操作者: {current_user.username}") + + try: + from app.models.document_model import Document + from app.models.end_user_model import EndUser + from app.models.app_model import App + from sqlalchemy import func + + # 通过 App 关联取该 workspace 下所有 end_user_id + end_user_ids = [ + str(u.id) for u in db.query(EndUser.id) + .join(App, EndUser.app_id == App.id) + .filter(App.workspace_id == workspace_id) + .all() + ] + if not end_user_ids: + return 0 + + file_names = [f"{uid}.txt" for uid in end_user_ids] + result = db.query(func.sum(Document.chunk_num)).filter( + Document.file_name.in_(file_names) + ).scalar() + + total_chunk = int(result or 0) + business_logger.info(f"成功获取用户知识库总chunk数: {total_chunk}") + return total_chunk + except Exception as e: + business_logger.error(f"获取用户知识库总chunk数失败: workspace_id={workspace_id} - {str(e)}") + raise + def get_current_user_total_chunk( end_user_id: str, db: Session, diff --git a/api/app/tasks.py b/api/app/tasks.py index 093f081f..4f7bfacc 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -62,7 +62,7 @@ def process_item(item: dict): @celery_app.task(name="app.core.rag.tasks.parse_document") -def parse_document(file_path: str, document_id: uuid.UUID): +def parse_document(file_path: str, document_id: str): """ Document parsing, vectorization, and storage """ @@ -74,6 +74,9 @@ def parse_document(file_path: str, document_id: uuid.UUID): db = next(get_db()) # Manually call the generator db_document = None db_knowledge = None + # 确保 document_id 是 UUID 对象 + if not isinstance(document_id, uuid.UUID): + document_id = uuid.UUID(str(document_id)) progress_msg = f"{datetime.now().strftime('%H:%M:%S')} Task has been received.\n" try: db_document = db.query(Document).filter(Document.id == document_id).first() @@ -286,11 +289,13 @@ def parse_document(file_path: str, document_id: uuid.UUID): result = f"parse document '{db_document.file_name}' processed successfully." return result except Exception as e: - if 'db_document' in locals(): - db_document.progress_msg += f"Failed to vectorize and import the parsed document:{str(e)}\n" + if db_document is not None: + db_document.progress_msg = (db_document.progress_msg or "") + f"Failed to vectorize and import the parsed document: {str(e)}\n" db_document.run = 0 db.commit() - result = f"parse document '{db_document.file_name}' failed." + result = f"parse document '{db_document.file_name}' failed." + else: + result = f"parse document '{document_id}' failed: document not found in DB. error={str(e)}" return result finally: db.close() From 817221347f7b5b34bf54c5be581719388bc69267 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 17:57:58 +0800 Subject: [PATCH 41/83] [fix] Preserve full result dict and default status to "unknown" instead of "success". --- api/app/services/memory_api_service.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api/app/services/memory_api_service.py b/api/app/services/memory_api_service.py index ad0a8164..f86fbed8 100644 --- a/api/app/services/memory_api_service.py +++ b/api/app/services/memory_api_service.py @@ -154,13 +154,17 @@ class MemoryAPIService: logger.info(f"Memory write successful for end_user: {end_user_id}") # result may be a string "success" or a dict with a "status" key + # Preserve the full dict so callers don't silently lose extra fields + # (e.g. error codes, metadata) returned by MemoryAgentService. if isinstance(result, dict): - status = result.get("status", "success") - else: - status = result if isinstance(result, str) else "success" + return { + **result, + "status": result.get("status", "unknown"), + "end_user_id": end_user_id, + } return { - "status": status, - "end_user_id": end_user_id + "status": result if isinstance(result, str) else "success", + "end_user_id": end_user_id, } except ConfigurationError as e: From 420f391f3c242ea4d485e44bcd5fa25ad6df9e29 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 18:01:56 +0800 Subject: [PATCH 42/83] [fix] Fixed tuple unpacking and moved UUID conversion into the try block. --- api/app/services/memory_dashboard_service.py | 2 +- api/app/tasks.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/app/services/memory_dashboard_service.py b/api/app/services/memory_dashboard_service.py index 22752805..05aed57e 100644 --- a/api/app/services/memory_dashboard_service.py +++ b/api/app/services/memory_dashboard_service.py @@ -423,7 +423,7 @@ def get_rag_user_kb_total_chunk( # 通过 App 关联取该 workspace 下所有 end_user_id end_user_ids = [ - str(u.id) for u in db.query(EndUser.id) + str(eid) for (eid,) in db.query(EndUser.id) .join(App, EndUser.app_id == App.id) .filter(App.workspace_id == workspace_id) .all() diff --git a/api/app/tasks.py b/api/app/tasks.py index 4f7bfacc..2846071a 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -74,11 +74,11 @@ def parse_document(file_path: str, document_id: str): db = next(get_db()) # Manually call the generator db_document = None db_knowledge = None - # 确保 document_id 是 UUID 对象 - if not isinstance(document_id, uuid.UUID): - document_id = uuid.UUID(str(document_id)) progress_msg = f"{datetime.now().strftime('%H:%M:%S')} Task has been received.\n" try: + # 确保 document_id 是 UUID 对象 + if not isinstance(document_id, uuid.UUID): + document_id = uuid.UUID(str(document_id)) db_document = db.query(Document).filter(Document.id == document_id).first() db_knowledge = db.query(Knowledge).filter(Knowledge.id == db_document.kb_id).first() # 1. Document parsing & segmentation From 8aad8faae9b40036486a7ef3e4bb4abd97d7aca2 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Mar 2026 18:05:54 +0800 Subject: [PATCH 43/83] fix(web): chat loading fix --- web/src/views/ApplicationConfig/components/Chat.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 8cb6812c..62f7c592 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-03-03 14:21:54 + * @Last Modified time: 2026-03-04 18:05:36 */ /** * Chat debugging component for application testing @@ -217,6 +217,8 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc } } if (!isCanSend) { + setLoading(false) + setCompareLoading(false) return } runCompare(data.app_id, { From 647a9788657e1ccea2e1b620d1141c2af28cf58e Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Wed, 4 Mar 2026 19:07:40 +0800 Subject: [PATCH 44/83] [fix] Restore task --- api/app/tasks.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/api/app/tasks.py b/api/app/tasks.py index 2846071a..093f081f 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -62,7 +62,7 @@ def process_item(item: dict): @celery_app.task(name="app.core.rag.tasks.parse_document") -def parse_document(file_path: str, document_id: str): +def parse_document(file_path: str, document_id: uuid.UUID): """ Document parsing, vectorization, and storage """ @@ -76,9 +76,6 @@ def parse_document(file_path: str, document_id: str): db_knowledge = None progress_msg = f"{datetime.now().strftime('%H:%M:%S')} Task has been received.\n" try: - # 确保 document_id 是 UUID 对象 - if not isinstance(document_id, uuid.UUID): - document_id = uuid.UUID(str(document_id)) db_document = db.query(Document).filter(Document.id == document_id).first() db_knowledge = db.query(Knowledge).filter(Knowledge.id == db_document.kb_id).first() # 1. Document parsing & segmentation @@ -289,13 +286,11 @@ def parse_document(file_path: str, document_id: str): result = f"parse document '{db_document.file_name}' processed successfully." return result except Exception as e: - if db_document is not None: - db_document.progress_msg = (db_document.progress_msg or "") + f"Failed to vectorize and import the parsed document: {str(e)}\n" + if 'db_document' in locals(): + db_document.progress_msg += f"Failed to vectorize and import the parsed document:{str(e)}\n" db_document.run = 0 db.commit() - result = f"parse document '{db_document.file_name}' failed." - else: - result = f"parse document '{document_id}' failed: document not found in DB. error={str(e)}" + result = f"parse document '{db_document.file_name}' failed." return result finally: db.close() From 590ec3a446d957f17e9d9e05189f2a155dd378ab Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Thu, 5 Mar 2026 09:55:54 +0800 Subject: [PATCH 45/83] feat(model and app): 1. Increase support for visual models and multimodal models; 2. The application and workflow can input various multimodal files such as images, documents, audio, and videos. --- api/app/controllers/app_controller.py | 3 +- api/app/controllers/model_controller.py | 4 +- api/app/controllers/ontology_controller.py | 17 +- api/app/core/agent/langchain_agent.py | 153 ++++--- api/app/core/models/base.py | 30 +- .../core/models/scripts/bedrock_models.yaml | 38 +- .../core/models/scripts/dashscope_models.yaml | 172 ++++++- api/app/core/models/scripts/loader.py | 45 +- .../core/models/scripts/openai_models.yaml | 64 ++- api/app/models/models_model.py | 13 +- api/app/schemas/app_schema.py | 16 +- api/app/schemas/model_schema.py | 18 + api/app/services/app_chat_service.py | 6 +- api/app/services/app_service.py | 2 +- .../services/audio_transcription_service.py | 101 +++++ .../services/collaborative_orchestrator.py | 2 + api/app/services/draft_run_service.py | 11 +- api/app/services/handoffs_service.py | 1 + api/app/services/llm_router.py | 1 + api/app/services/master_agent_router.py | 1 + api/app/services/model_service.py | 58 ++- api/app/services/multi_agent_orchestrator.py | 2 + api/app/services/multi_agent_service.py | 2 +- api/app/services/multimodal_service.py | 426 +++++++++++++----- api/app/services/prompt_optimizer_service.py | 3 +- api/app/services/shared_chat_service.py | 2 + 26 files changed, 958 insertions(+), 233 deletions(-) create mode 100644 api/app/services/audio_transcription_service.py diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index e2849ad6..653f616c 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -835,7 +835,8 @@ async def draft_run_compare( web_search=True, memory=True, parallel=payload.parallel, - timeout=payload.timeout or 60 + timeout=payload.timeout or 60, + files=payload.files ) logger.info( diff --git a/api/app/controllers/model_controller.py b/api/app/controllers/model_controller.py index bb1ba526..0de3d4fe 100644 --- a/api/app/controllers/model_controller.py +++ b/api/app/controllers/model_controller.py @@ -469,7 +469,9 @@ async def create_model_api_key_by_provider( config=api_key_data.config, is_active=api_key_data.is_active, priority=api_key_data.priority, - model_config_ids=model_config_ids + model_config_ids=model_config_ids, + capability=api_key_data.capability, + is_omni=api_key_data.is_omni ) created_keys, failed_models = await ModelApiKeyService.create_api_key_by_provider(db=db, data=create_data) diff --git a/api/app/controllers/ontology_controller.py b/api/app/controllers/ontology_controller.py index e4a87141..42d4bee0 100644 --- a/api/app/controllers/ontology_controller.py +++ b/api/app/controllers/ontology_controller.py @@ -124,15 +124,23 @@ def _get_ontology_service( ) # 通过 Repository 获取可用的 API Key(负载均衡逻辑由 Repository 处理) - from app.repositories.model_repository import ModelApiKeyRepository - api_keys = ModelApiKeyRepository.get_by_model_config(db, model_config.id) - if not api_keys: + # from app.repositories.model_repository import ModelApiKeyRepository + from app.services.model_service import ModelApiKeyService + api_key_config = ModelApiKeyService.get_available_api_key(db, model_config.id) + if not api_key_config: logger.error(f"Model {llm_id} has no active API key") raise HTTPException( status_code=400, detail="指定的LLM模型没有可用的API密钥" ) - api_key_config = api_keys[0] + # api_keys = ModelApiKeyRepository.get_by_model_config(db, model_config.id) + # if not api_keys: + # logger.error(f"Model {llm_id} has no active API key") + # raise HTTPException( + # status_code=400, + # detail="指定的LLM模型没有可用的API密钥" + # ) + # api_key_config = api_keys[0] is_composite = getattr(model_config, 'is_composite', False) logger.info( @@ -154,6 +162,7 @@ def _get_ontology_service( provider=actual_provider, api_key=api_key_config.api_key, base_url=api_key_config.api_base, + is_omni=api_key_config.is_omni, max_retries=3, timeout=60.0 ) diff --git a/api/app/core/agent/langchain_agent.py b/api/app/core/agent/langchain_agent.py index fae20ea2..88b6371c 100644 --- a/api/app/core/agent/langchain_agent.py +++ b/api/app/core/agent/langchain_agent.py @@ -11,35 +11,37 @@ LangChain Agent 封装 import time from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence -from app.core.memory.agent.langgraph_graph.write_graph import write_long_term +from app.core.memory.agent.langgraph_graph.write_graph import write_long_term from app.db import get_db from app.core.logging_config import get_business_logger from app.core.models import RedBearLLM, RedBearModelConfig -from app.models.models_model import ModelType +from app.models.models_model import ModelType, ModelProvider from app.services.memory_agent_service import ( get_end_user_connected_config, ) from langchain.agents import create_agent from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from langchain_core.tools import BaseTool + logger = get_business_logger() class LangChainAgent: def __init__( - self, - model_name: str, - api_key: str, - provider: str = "openai", - api_base: Optional[str] = None, - temperature: float = 0.7, - max_tokens: int = 2000, - system_prompt: Optional[str] = None, - tools: Optional[Sequence[BaseTool]] = None, - streaming: bool = False, - max_iterations: Optional[int] = None, # 最大迭代次数(None 表示自动计算) - max_tool_consecutive_calls: int = 3 # 单个工具最大连续调用次数 + self, + model_name: str, + api_key: str, + provider: str = "openai", + api_base: Optional[str] = None, + is_omni: bool = False, + temperature: float = 0.7, + max_tokens: int = 2000, + system_prompt: Optional[str] = None, + tools: Optional[Sequence[BaseTool]] = None, + streaming: bool = False, + max_iterations: Optional[int] = None, # 最大迭代次数(None 表示自动计算) + max_tool_consecutive_calls: int = 3 # 单个工具最大连续调用次数 ): """初始化 LangChain Agent @@ -60,12 +62,13 @@ class LangChainAgent: self.provider = provider self.tools = tools or [] self.streaming = streaming + self.is_omni = is_omni self.max_tool_consecutive_calls = max_tool_consecutive_calls - + # 工具调用计数器:记录每个工具的连续调用次数 self.tool_call_counter: Dict[str, int] = {} self.last_tool_called: Optional[str] = None - + # 根据工具数量动态调整最大迭代次数 # 基础值 + 每个工具额外的调用机会 if max_iterations is None: @@ -73,9 +76,9 @@ class LangChainAgent: self.max_iterations = 5 + len(self.tools) * 2 else: self.max_iterations = max_iterations - + self.system_prompt = system_prompt or "你是一个专业的AI助手" - + logger.debug( f"Agent 迭代次数配置: max_iterations={self.max_iterations}, " f"tool_count={len(self.tools)}, " @@ -89,6 +92,7 @@ class LangChainAgent: provider=provider, api_key=api_key, base_url=api_base, + is_omni=is_omni, extra_params={ "temperature": temperature, "max_tokens": max_tokens, @@ -143,21 +147,22 @@ class LangChainAgent: """ from langchain_core.tools import StructuredTool from functools import wraps - + wrapped_tools = [] - + for original_tool in tools: tool_name = original_tool.name original_func = original_tool.func if hasattr(original_tool, 'func') else None - + if not original_func: # 如果无法获取原始函数,直接使用原工具 wrapped_tools.append(original_tool) continue - + # 创建包装函数 def make_wrapped_func(tool_name, original_func): """创建包装函数的工厂函数,避免闭包问题""" + @wraps(original_func) def wrapped_func(*args, **kwargs): """包装后的工具函数,跟踪连续调用次数""" @@ -168,13 +173,13 @@ class LangChainAgent: # 切换到新工具,重置计数器 self.tool_call_counter[tool_name] = 1 self.last_tool_called = tool_name - + current_count = self.tool_call_counter[tool_name] - + logger.debug( f"工具调用: {tool_name}, 连续调用次数: {current_count}/{self.max_tool_consecutive_calls}" ) - + # 检查是否超过最大连续调用次数 if current_count > self.max_tool_consecutive_calls: logger.warning( @@ -185,12 +190,12 @@ class LangChainAgent: f"工具 '{tool_name}' 已连续调用 {self.max_tool_consecutive_calls} 次," f"未找到有效结果。请尝试其他方法或直接回答用户的问题。" ) - + # 调用原始工具函数 return original_func(*args, **kwargs) - + return wrapped_func - + # 使用 StructuredTool 创建新工具 wrapped_tool = StructuredTool( name=original_tool.name, @@ -198,17 +203,17 @@ class LangChainAgent: func=make_wrapped_func(tool_name, original_func), args_schema=original_tool.args_schema if hasattr(original_tool, 'args_schema') else None ) - + wrapped_tools.append(wrapped_tool) - + return wrapped_tools def _prepare_messages( - self, - message: str, - history: Optional[List[Dict[str, str]]] = None, - context: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None + self, + message: str, + history: Optional[List[Dict[str, str]]] = None, + context: Optional[str] = None, + files: Optional[List[Dict[str, Any]]] = None ) -> List[BaseMessage]: """准备消息列表 @@ -248,7 +253,7 @@ class LangChainAgent: messages.append(HumanMessage(content=user_content)) return messages - + def _build_multimodal_content(self, text: str, files: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ 构建多模态消息内容 @@ -261,23 +266,26 @@ class LangChainAgent: List[Dict]: 消息内容列表 """ # 根据 provider 使用不同的文本格式 - if self.provider.lower() in ["bedrock", "anthropic"]: - # Anthropic/Bedrock: {"type": "text", "text": "..."} - content_parts = [{"type": "text", "text": text}] - else: - # 通义千问等: {"text": "..."} - content_parts = [{"text": text}] - + # if (self.provider.lower() in [ModelProvider.BEDROCK, ModelProvider.OPENAI, ModelProvider.XINFERENCE, + # ModelProvider.GPUSTACK] or ( + # self.provider.lower() == ModelProvider.DASHSCOPE and self.is_omni)): + # # Anthropic/Bedrock/Xinference/Gpustack/Openai: {"type": "text", "text": "..."} + # content_parts = [{"type": "text", "text": text}] + # else: + # # 通义千问等: {"text": "..."} + # content_parts = [{"type": "text", "text": text}] + content_parts = [{"type": "text", "text": text}] + # 添加文件内容 # MultimodalService 已经根据 provider 返回了正确格式,直接使用 content_parts.extend(files) - + logger.debug( f"构建多模态消息: provider={self.provider}, " f"parts={len(content_parts)}, " f"files={len(files)}" ) - + return content_parts async def chat( @@ -302,7 +310,7 @@ class LangChainAgent: Returns: Dict: 包含 content 和元数据的字典 """ - message_chat= message + message_chat = message start_time = time.time() actual_config_id = config_id # If config_id is None, try to get from end_user's connected config @@ -322,8 +330,8 @@ class LangChainAgent: except Exception as e: logger.warning(f"Failed to get db session: {e}") actual_end_user_id = end_user_id if end_user_id is not None else "unknown" - logger.info(f'写入类型{storage_type,str(end_user_id), message, str(user_rag_memory_id)}') - print(f'写入类型{storage_type,str(end_user_id), message, str(user_rag_memory_id)}') + logger.info(f'写入类型{storage_type, str(end_user_id), message, str(user_rag_memory_id)}') + print(f'写入类型{storage_type, str(end_user_id), message, str(user_rag_memory_id)}') try: # 准备消息列表(支持多模态) messages = self._prepare_messages(message, history, context, files) @@ -367,14 +375,14 @@ class LangChainAgent: # 获取最后的 AI 消息 output_messages = result.get("messages", []) content = "" - + logger.debug(f"输出消息数量: {len(output_messages)}") total_tokens = 0 for msg in reversed(output_messages): if isinstance(msg, AIMessage): logger.debug(f"找到 AI 消息,content 类型: {type(msg.content)}") logger.debug(f"AI 消息内容: {msg.content}") - + # 处理多模态响应:content 可能是字符串或列表 if isinstance(msg.content, str): content = msg.content @@ -407,12 +415,13 @@ class LangChainAgent: response_meta = msg.response_metadata if hasattr(msg, 'response_metadata') else None total_tokens = response_meta.get("token_usage", {}).get("total_tokens", 0) if response_meta else 0 break - + logger.info(f"最终提取的内容长度: {len(content)}") elapsed_time = time.time() - start_time if memory_flag: - await write_long_term(storage_type, end_user_id, message_chat, content, user_rag_memory_id, actual_config_id) + await write_long_term(storage_type, end_user_id, message_chat, content, user_rag_memory_id, + actual_config_id) response = { "content": content, "model": self.model_name, @@ -439,16 +448,16 @@ class LangChainAgent: raise async def chat_stream( - self, - message: str, - history: Optional[List[Dict[str, str]]] = None, - context: Optional[str] = None, - end_user_id:Optional[str] = None, - config_id: Optional[str] = None, - storage_type:Optional[str] = None, - user_rag_memory_id:Optional[str] = None, - memory_flag: Optional[bool] = True, - files: Optional[List[Dict[str, Any]]] = None # 新增:多模态文件 + self, + message: str, + history: Optional[List[Dict[str, str]]] = None, + context: Optional[str] = None, + end_user_id: Optional[str] = None, + config_id: Optional[str] = None, + storage_type: Optional[str] = None, + user_rag_memory_id: Optional[str] = None, + memory_flag: Optional[bool] = True, + files: Optional[List[Dict[str, Any]]] = None # 新增:多模态文件 ) -> AsyncGenerator[str, None]: """执行流式对话 @@ -482,7 +491,6 @@ class LangChainAgent: except Exception as e: logger.warning(f"Failed to get db session: {e}") - # 注意:不在这里写入用户消息,等 AI 回复后一起写入 try: # 准备消息列表(支持多模态) @@ -500,13 +508,13 @@ class LangChainAgent: full_content = '' try: async for event in self.agent.astream_events( - {"messages": messages}, - version="v2", - config={"recursion_limit": self.max_iterations} + {"messages": messages}, + version="v2", + config={"recursion_limit": self.max_iterations} ): chunk_count += 1 kind = event.get("event") - + # 处理所有可能的流式事件 if kind == "on_chat_model_stream": # LLM 流式输出 @@ -540,7 +548,7 @@ class LangChainAgent: full_content += item yield item yielded_content = True - + elif kind == "on_llm_stream": # 另一种 LLM 流式事件 chunk = event.get("data", {}).get("chunk") @@ -577,13 +585,13 @@ class LangChainAgent: full_content += chunk yield chunk yielded_content = True - + # 记录工具调用(可选) elif kind == "on_tool_start": logger.debug(f"工具调用开始: {event.get('name')}") elif kind == "on_tool_end": logger.debug(f"工具调用结束: {event.get('name')}") - + logger.debug(f"Agent 流式完成,共 {chunk_count} 个事件") # 统计token消耗 output_messages = event.get("data", {}).get("output", {}).get("messages", []) @@ -595,7 +603,8 @@ class LangChainAgent: yield total_tokens break if memory_flag: - await write_long_term(storage_type, end_user_id, message_chat, full_content, user_rag_memory_id, actual_config_id) + await write_long_term(storage_type, end_user_id, message_chat, full_content, user_rag_memory_id, + actual_config_id) except Exception as e: logger.error(f"Agent astream_events 失败: {str(e)}", exc_info=True) raise @@ -609,5 +618,3 @@ class LangChainAgent: logger.info("=" * 80) logger.info("chat_stream 方法执行结束") logger.info("=" * 80) - - diff --git a/api/app/core/models/base.py b/api/app/core/models/base.py index f5f49af0..5d4dbd10 100644 --- a/api/app/core/models/base.py +++ b/api/app/core/models/base.py @@ -27,6 +27,7 @@ class RedBearModelConfig(BaseModel): provider: str api_key: str base_url: Optional[str] = None + is_omni: bool = False # 是否为 Omni 模型 # 请求超时时间(秒)- 默认120秒以支持复杂的LLM调用,可通过环境变量 LLM_TIMEOUT 配置 timeout: float = Field(default_factory=lambda: float(os.getenv("LLM_TIMEOUT", "120.0"))) # 最大重试次数 - 默认2次以避免过长等待,可通过环境变量 LLM_MAX_RETRIES 配置 @@ -45,7 +46,28 @@ class RedBearModelFactory: # 打印供应商信息用于调试 from app.core.logging_config import get_business_logger logger = get_business_logger() - logger.debug(f"获取模型参数 - Provider: {provider}, Model: {config.model_name}") + logger.debug(f"获取模型参数 - Provider: {provider}, Model: {config.model_name}, is_omni: {config.is_omni}") + + # dashscope 的 omni 模型使用 OpenAI 兼容模式 + if provider == ModelProvider.DASHSCOPE and config.is_omni: + import httpx + if not config.base_url: + config.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" + timeout_config = httpx.Timeout( + timeout=config.timeout, + connect=60.0, + read=config.timeout, + write=60.0, + pool=10.0, + ) + return { + "model": config.model_name, + "base_url": config.base_url, + "api_key": config.api_key, + "timeout": timeout_config, + "max_retries": config.max_retries, + **config.extra_params + } if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK, ModelProvider.OLLAMA]: # 使用 httpx.Timeout 对象来设置详细的超时配置 @@ -135,6 +157,12 @@ class RedBearModelFactory: def get_provider_llm_class(config:RedBearModelConfig, type: ModelType=ModelType.LLM) -> type[BaseLLM]: """根据模型提供商获取对应的模型类""" provider = config.provider.lower() + + # dashscope 的 omni 模型使用 OpenAI 兼容模式 + if provider == ModelProvider.DASHSCOPE and config.is_omni: + from langchain_openai import ChatOpenAI + return ChatOpenAI + if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK] : if type == ModelType.LLM: from langchain_openai import OpenAI diff --git a/api/app/core/models/scripts/bedrock_models.yaml b/api/app/core/models/scripts/bedrock_models.yaml index e5b91d1c..2c0ab757 100644 --- a/api/app/core/models/scripts/bedrock_models.yaml +++ b/api/app/core/models/scripts/bedrock_models.yaml @@ -6,6 +6,8 @@ models: description: AI21 Labs大语言模型,completion生成模式,256000上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 logo: bedrock @@ -15,6 +17,9 @@ models: description: Amazon Nova大语言模型,支持智能体思考、工具调用、流式工具调用、视觉能力,300000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -28,6 +33,9 @@ models: description: Anthropic Claude大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用、文档处理,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -42,6 +50,8 @@ models: description: Cohere大语言模型,支持智能体思考、工具调用、流式工具调用,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -54,6 +64,9 @@ models: description: DeepSeek大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用,32768上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -67,6 +80,8 @@ models: description: Meta Llama大语言模型,支持智能体思考、工具调用,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -78,6 +93,8 @@ models: description: Mistral AI大语言模型,支持智能体思考、工具调用,32000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -89,6 +106,8 @@ models: description: OpenAI大语言模型,支持智能体思考、工具调用、流式工具调用,32768上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -101,6 +120,8 @@ models: description: Qwen大语言模型,支持智能体思考、工具调用、流式工具调用,32768上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -113,6 +134,8 @@ models: description: amazon.rerank-v1:0重排序模型,5120上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 重排序模型 logo: bedrock @@ -122,6 +145,8 @@ models: description: cohere.rerank-v3-5:0重排序模型,5120上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 重排序模型 logo: bedrock @@ -131,6 +156,9 @@ models: description: amazon.nova-2-multimodal-embeddings-v1:0文本嵌入模型,支持视觉能力,8192上下文窗口 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 文本嵌入模型 - vision @@ -141,6 +169,8 @@ models: description: amazon.titan-embed-text-v1文本嵌入模型,8192上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本嵌入模型 logo: bedrock @@ -150,6 +180,8 @@ models: description: amazon.titan-embed-text-v2:0文本嵌入模型,8192上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本嵌入模型 logo: bedrock @@ -159,6 +191,8 @@ models: description: Cohere Embed 3 English文本嵌入模型,512上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本嵌入模型 logo: bedrock @@ -168,6 +202,8 @@ models: description: Cohere Embed 3 Multilingual文本嵌入模型,512上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本嵌入模型 - logo: bedrock + logo: bedrock \ No newline at end of file diff --git a/api/app/core/models/scripts/dashscope_models.yaml b/api/app/core/models/scripts/dashscope_models.yaml index af1c3619..89a16966 100644 --- a/api/app/core/models/scripts/dashscope_models.yaml +++ b/api/app/core/models/scripts/dashscope_models.yaml @@ -6,6 +6,8 @@ models: description: DeepSeek-R1-Distill-Qwen-14B大语言模型,支持智能体思考,32000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -16,6 +18,8 @@ models: description: DeepSeek-R1-Distill-Qwen-32B大语言模型,支持智能体思考,32000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -26,6 +30,8 @@ models: description: DeepSeek-R1大语言模型,支持智能体思考,131072超大上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -36,6 +42,8 @@ models: description: DeepSeek-V3.1大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -46,6 +54,8 @@ models: description: DeepSeek-V3.2-exp实验版大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -56,6 +66,8 @@ models: description: DeepSeek-V3.2大语言模型,支持智能体思考,131072超大上下文窗口,对话模式,支持丰富生成参数调节 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -66,6 +78,8 @@ models: description: DeepSeek-V3大语言模型,支持智能体思考,64000上下文窗口,对话模式,支持文本与JSON格式输出 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -76,6 +90,8 @@ models: description: farui-plus大语言模型,支持多工具调用、智能体思考、流式工具调用,12288上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -88,6 +104,8 @@ models: description: GLM-4.7大语言模型,支持多工具调用、智能体思考、流式工具调用,202752超大上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -100,6 +118,9 @@ models: description: qvq-max-latest大语言模型,支持视觉、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - vision @@ -112,6 +133,9 @@ models: description: qvq-max大语言模型,支持视觉、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - vision @@ -124,6 +148,8 @@ models: description: qwen-coder-turbo-0919代码专用大语言模型,支持智能体思考,131072上下文窗口,对话模式,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 代码模型 @@ -135,6 +161,8 @@ models: description: qwen-max-latest大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -147,6 +175,8 @@ models: description: qwen-max-longcontext长上下文大语言模型,支持多工具调用、智能体思考、流式工具调用,32000上下文窗口,对话模式,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -159,6 +189,8 @@ models: description: qwen-max大语言模型,支持多工具调用、智能体思考、流式工具调用,32768上下文窗口,对话模式,支持联网搜索 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -171,6 +203,8 @@ models: description: qwen-mt-plus多语言翻译大语言模型,支持智能体思考,16384上下文窗口,对话模式,支持多语种互译与领域翻译适配 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 翻译模型 @@ -182,6 +216,8 @@ models: description: qwen-mt-turbo轻量化多语言翻译大语言模型,支持智能体思考,16384上下文窗口,对话模式,支持多语种互译与领域翻译适配 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 翻译模型 @@ -193,6 +229,8 @@ models: description: qwen-plus-0112大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -205,6 +243,8 @@ models: description: qwen-plus-0125大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -217,6 +257,8 @@ models: description: qwen-plus-0723大语言模型,支持多工具调用、智能体思考、流式工具调用,32000上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -229,6 +271,8 @@ models: description: qwen-plus-0806大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -241,6 +285,8 @@ models: description: qwen-plus-0919大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -253,6 +299,8 @@ models: description: qwen-plus-1125大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -265,6 +313,8 @@ models: description: qwen-plus-1127大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,支持联网搜索,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -277,6 +327,8 @@ models: description: qwen-plus-1220大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -289,6 +341,10 @@ models: description: qwen-vl-max多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -302,6 +358,10 @@ models: description: qwen-vl-plus-0809多模态大模型,支持视觉理解、智能体思考、视频理解,32768上下文窗口,对话模式,已废弃 is_deprecated: true is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -315,6 +375,10 @@ models: description: qwen-vl-plus-2025-01-02多模态大模型,支持视觉理解、智能体思考、视频理解,32768上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -328,6 +392,10 @@ models: description: qwen-vl-plus-2025-01-25多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -341,6 +409,10 @@ models: description: qwen-vl-plus-latest多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -354,6 +426,10 @@ models: description: qwen-vl-plus多模态大模型,支持视觉理解、智能体思考、视频理解,131072上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -367,6 +443,8 @@ models: description: qwen2.5-0.5b-instruct大语言模型,支持多工具调用、智能体思考、流式工具调用,32768上下文窗口,对话模式,未废弃 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -379,6 +457,8 @@ models: description: qwen3-14b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -391,6 +471,8 @@ models: description: qwen3-235b-a22b-instruct-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -403,6 +485,8 @@ models: description: qwen3-235b-a22b-thinking-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -415,6 +499,8 @@ models: description: qwen3-235b-a22b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -427,6 +513,8 @@ models: description: qwen3-30b-a3b-instruct-2507大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -439,6 +527,8 @@ models: description: qwen3-30b-a3b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -451,6 +541,8 @@ models: description: qwen3-32b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -463,6 +555,8 @@ models: description: qwen3-4b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -475,6 +569,8 @@ models: description: qwen3-8b大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -487,6 +583,8 @@ models: description: qwen3-coder-30b-a3b-instruct大语言模型,支持智能体思考,262144上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 代码模型 @@ -498,6 +596,8 @@ models: description: qwen3-coder-480b-a35b-instruct大语言模型,支持智能体思考,262144上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 代码模型 @@ -509,6 +609,8 @@ models: description: qwen3-coder-plus-2025-09-23大语言模型,支持智能体思考,1000000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 代码模型 @@ -520,6 +622,8 @@ models: description: qwen3-coder-plus大语言模型,支持智能体思考,1000000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - 代码模型 @@ -531,6 +635,8 @@ models: description: qwen3-max-2025-09-23大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -544,6 +650,8 @@ models: description: qwen3-max-2026-01-23大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -557,6 +665,8 @@ models: description: qwen3-max-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -569,6 +679,8 @@ models: description: qwen3-max大语言模型,支持多工具调用、智能体思考、流式工具调用,262144上下文窗口,对话模式,支持联网搜索 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -582,6 +694,8 @@ models: description: qwen3-next-80b-a3b-instruct大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -594,6 +708,8 @@ models: description: qwen3-next-80b-a3b-thinking大语言模型,支持多工具调用、智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -606,6 +722,11 @@ models: description: qwen3-omni-flash-2025-12-01多模态大语言模型,支持视觉、智能体思考、视频、音频能力,65536上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + - audio + is_omni: true tags: - 大语言模型 - 多模态模型 @@ -620,6 +741,10 @@ models: description: qwen3-vl-235b-a22b-instruct多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -635,6 +760,10 @@ models: description: qwen3-vl-235b-a22b-thinking多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -650,6 +779,10 @@ models: description: qwen3-vl-30b-a3b-instruct多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -665,6 +798,10 @@ models: description: qwen3-vl-30b-a3b-thinking多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -680,6 +817,10 @@ models: description: qwen3-vl-flash多模态大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉、视频能力,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -695,6 +836,10 @@ models: description: qwen3-vl-plus-2025-09-23多模态大语言模型,支持视觉、智能体思考、视频能力,262144上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -708,6 +853,10 @@ models: description: qwen3-vl-plus多模态大语言模型,支持视觉、智能体思考、视频能力,262144上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - video + is_omni: false tags: - 大语言模型 - 多模态模型 @@ -721,6 +870,8 @@ models: description: qwq-32b大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -732,6 +883,8 @@ models: description: qwq-plus-0305大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -743,6 +896,8 @@ models: description: qwq-plus大语言模型,支持智能体思考、流式工具调用,131072上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -754,6 +909,8 @@ models: description: gte-rerank-v2重排序模型,4000上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 重排序模型 logo: dashscope @@ -763,6 +920,8 @@ models: description: gte-rerank重排序模型,4000上下文窗口 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 重排序模型 logo: dashscope @@ -772,6 +931,9 @@ models: description: multimodal-embedding-v1多模态嵌入模型,支持视觉能力,8192上下文窗口,最大分块数10 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 嵌入模型 - 多模态模型 @@ -783,6 +945,8 @@ models: description: text-embedding-v1文本嵌入模型,2048上下文窗口,最大分块数25 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 嵌入模型 - 文本嵌入 @@ -793,6 +957,8 @@ models: description: text-embedding-v2文本嵌入模型,2048上下文窗口,最大分块数25 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 嵌入模型 - 文本嵌入 @@ -803,6 +969,8 @@ models: description: text-embedding-v3文本嵌入模型,8192上下文窗口,最大分块数10 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 嵌入模型 - 文本嵌入 @@ -813,7 +981,9 @@ models: description: text-embedding-v4文本嵌入模型,8192上下文窗口,最大分块数10 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 嵌入模型 - 文本嵌入 - logo: dashscope + logo: dashscope \ No newline at end of file diff --git a/api/app/core/models/scripts/loader.py b/api/app/core/models/scripts/loader.py index a14d3268..e4462efa 100644 --- a/api/app/core/models/scripts/loader.py +++ b/api/app/core/models/scripts/loader.py @@ -6,7 +6,7 @@ from typing import Callable import yaml from sqlalchemy.orm import Session -from app.models.models_model import ModelBase, ModelProvider +from app.models.models_model import ModelBase, ModelProvider, ModelConfig def _load_yaml_config(provider: ModelProvider) -> list[dict]: @@ -55,6 +55,15 @@ def load_models(db: Session, providers: list[str] = None, silent: bool = False) print(f"\n正在加载 {provider.value} 的 {len(models)} 个模型...") for model_data in models: + config_sync_fields = { + "logo": None, + "capability": None, + "is_omni": None, + "name": None, + "provider": None, + "type": None, + "description": None + } try: # 检查模型是否已存在 existing = db.query(ModelBase).filter( @@ -66,6 +75,40 @@ def load_models(db: Session, providers: list[str] = None, silent: bool = False) # 更新现有模型配置 for key, value in model_data.items(): setattr(existing, key, value) + + # 更新绑定了该 model_id 的 ModelConfig 和 ModelApiKey + sync_fields = [k for k in config_sync_fields.keys() if k in model_data] + if sync_fields: + # 批量更新 ModelConfig + update_kwargs = {k: model_data[k] for k in sync_fields} + db.query(ModelConfig).filter(ModelConfig.model_id == existing.id).update( + update_kwargs, + synchronize_session=False + ) + + # 更新 ModelApiKey 的 capability 和 is_omni + if 'capability' in model_data or 'is_omni' in model_data: + from app.models.models_model import ModelApiKey, model_config_api_key_association + api_key_update = {} + if 'capability' in model_data: + api_key_update['capability'] = model_data['capability'] + if 'is_omni' in model_data: + api_key_update['is_omni'] = model_data['is_omni'] + + if api_key_update: + # 查找所有关联的 API Key + api_key_ids = db.query(model_config_api_key_association.c.api_key_id).join( + ModelConfig, + ModelConfig.id == model_config_api_key_association.c.model_config_id + ).filter(ModelConfig.model_id == existing.id).distinct().all() + + if api_key_ids: + api_key_ids = [aid[0] for aid in api_key_ids] + db.query(ModelApiKey).filter(ModelApiKey.id.in_(api_key_ids)).update( + api_key_update, + synchronize_session=False + ) + db.commit() if not silent: print(f"更新成功: {model_data['name']}") diff --git a/api/app/core/models/scripts/openai_models.yaml b/api/app/core/models/scripts/openai_models.yaml index 68c63ee2..7f6d3a51 100644 --- a/api/app/core/models/scripts/openai_models.yaml +++ b/api/app/core/models/scripts/openai_models.yaml @@ -6,12 +6,19 @@ models: description: chatgpt-4o-latest大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + - audio + - video + is_omni: true tags: - 大语言模型 - multi-tool-call - agent-thought - stream-tool-call - vision + - audio + - video logo: openai - name: gpt-3.5-turbo-0125 type: llm @@ -19,6 +26,8 @@ models: description: gpt-3.5-turbo-0125大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -31,6 +40,8 @@ models: description: gpt-3.5-turbo-1106大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -43,6 +54,8 @@ models: description: gpt-3.5-turbo-16k大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -55,6 +68,8 @@ models: description: gpt-3.5-turbo-instruct大语言模型,4096上下文窗口,文本补全模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 logo: openai @@ -64,6 +79,8 @@ models: description: gpt-3.5-turbo大语言模型,支持多工具调用、智能体思考、流式工具调用,16385上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -76,6 +93,8 @@ models: description: gpt-4-0125-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -88,6 +107,8 @@ models: description: gpt-4-1106-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -100,6 +121,9 @@ models: description: gpt-4-turbo-2024-04-09大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -113,6 +137,8 @@ models: description: gpt-4-turbo-preview大语言模型,支持多工具调用、智能体思考、流式工具调用,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -125,6 +151,9 @@ models: description: gpt-4-turbo大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力,128000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -138,6 +167,8 @@ models: description: o1-preview大语言模型,支持智能体思考,128000上下文窗口,对话模式,已废弃 is_deprecated: true is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -148,6 +179,9 @@ models: description: o1大语言模型,支持多工具调用、智能体思考、流式工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - multi-tool-call @@ -162,6 +196,9 @@ models: description: o3-2025-04-16大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -176,6 +213,8 @@ models: description: o3-mini-2025-01-31大语言模型,支持智能体思考、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -189,6 +228,8 @@ models: description: o3-mini大语言模型,支持智能体思考、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 大语言模型 - agent-thought @@ -202,6 +243,9 @@ models: description: o3-pro-2025-06-10大语言模型,支持智能体思考、工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -215,6 +259,9 @@ models: description: o3-pro大语言模型,支持智能体思考、工具调用、视觉能力、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -228,6 +275,9 @@ models: description: o3大语言模型,支持智能体思考、视觉能力、工具调用、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -242,6 +292,9 @@ models: description: o4-mini-2025-04-16大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -256,6 +309,9 @@ models: description: o4-mini大语言模型,支持智能体思考、工具调用、视觉能力、流式工具调用、结构化输出,200000上下文窗口,对话模式 is_deprecated: false is_official: true + capability: + - vision + is_omni: false tags: - 大语言模型 - agent-thought @@ -270,6 +326,8 @@ models: description: text-embedding-3-large文本向量模型,8191上下文窗口,最大分块数32 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本向量模型 logo: openai @@ -279,6 +337,8 @@ models: description: text-embedding-3-small文本向量模型,8191上下文窗口,最大分块数32 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本向量模型 logo: openai @@ -288,6 +348,8 @@ models: description: text-embedding-ada-002文本向量模型,8097上下文窗口,最大分块数32 is_deprecated: false is_official: true + capability: [] + is_omni: false tags: - 文本向量模型 - logo: openai + logo: openai \ No newline at end of file diff --git a/api/app/models/models_model.py b/api/app/models/models_model.py index 3e378f17..23fafcef 100644 --- a/api/app/models/models_model.py +++ b/api/app/models/models_model.py @@ -2,7 +2,7 @@ import datetime import uuid from enum import StrEnum -from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, UniqueConstraint, Integer, ARRAY, Table +from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, UniqueConstraint, Integer, ARRAY, Table, text from sqlalchemy.dialects.postgresql import UUID, JSON from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -78,6 +78,9 @@ class ModelConfig(BaseModel): description = Column(String, comment="模型描述") # 模型配置参数 + capability = Column(ARRAY(String), default=list, nullable=False, server_default=text("'{}'::varchar[]"), + comment="模型能力列表(如['vision', 'audio', 'video'])") + is_omni = Column(Boolean, default=False, nullable=False, server_default="false", comment="是否为Omni模型(使用特殊API调用)") config = Column(JSON, comment="模型配置参数") # - temperature : 控制生成文本的随机性。值越高,输出越随机、越有创造性;值越低,输出越确定、越保守。 # - top_p : 一种替代 temperature 的采样方法,控制模型从概率最高的词中选择的范围。 @@ -118,6 +121,11 @@ class ModelApiKey(BaseModel): api_key = Column(String, nullable=False, comment="API密钥") api_base = Column(String, comment="API基础URL") + # 模型能力参数 + capability = Column(ARRAY(String), default=list, nullable=False, server_default=text("'{}'::varchar[]"), + comment="模型能力列表(如['vision', 'audio', 'video'])") + is_omni = Column(Boolean, default=False, nullable=False, server_default="false", comment="是否为Omni模型(使用特殊API调用)") + # 配置参数 config = Column(JSON, comment="API Key特定配置") @@ -155,6 +163,9 @@ class ModelBase(Base): tags = Column(ARRAY(String), default=list, nullable=False, comment="模型标签(如['聊天', '创作'])") add_count = Column(Integer, default=0, nullable=False, comment="模型被用户添加的次数") created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间", server_default=func.now()) + capability = Column(ARRAY(String), default=list, nullable=False, server_default=text("'{}'::varchar[]"), + comment="模型能力列表(如['vision', 'audio', 'video'])") + is_omni = Column(Boolean, default=False, nullable=False, server_default="false", comment="是否为Omni模型(使用特殊API调用)") # 关联关系 configs = relationship("ModelConfig", back_populates="model_base", cascade="all, delete-orphan") diff --git a/api/app/schemas/app_schema.py b/api/app/schemas/app_schema.py index 07875e13..f073a200 100644 --- a/api/app/schemas/app_schema.py +++ b/api/app/schemas/app_schema.py @@ -21,8 +21,14 @@ class FileType(StrEnum): def trans(cls, value: str) -> 'FileType': if value.startswith("image"): return cls.IMAGE - # TODO: other file type support - raise RuntimeError("Unsupport file type") + elif value.startswith("document"): + return cls.DOCUMENT + elif value.startswith("audio"): + return cls.AUDIO + elif value.startswith("video"): + return cls.VIDEO + else: + raise RuntimeError("Unsupport file type") class TransferMethod(str, Enum): @@ -37,6 +43,12 @@ class FileInput(BaseModel): transfer_method: TransferMethod = Field(..., description="传输方式: local_file/remote_url") upload_file_id: Optional[uuid.UUID] = Field(None, description="已上传文件ID(local_file时必填)") url: Optional[str] = Field(None, description="远程URL(remote_url时必填)") + file_type: Optional[str] = Field(None, description="具体文件格式(如image/jpg、audio/wav、document/docx、video/mp4)") + + def __init__(self, **data): + if "type" in data: + data['file_type'] = data['type'] + super().__init__(**data) @field_validator("type", mode="before") @classmethod diff --git a/api/app/schemas/model_schema.py b/api/app/schemas/model_schema.py index 0c0bbeed..f25d9408 100644 --- a/api/app/schemas/model_schema.py +++ b/api/app/schemas/model_schema.py @@ -21,6 +21,8 @@ class ModelConfigBase(BaseModel): is_active: bool = Field(True, description="是否激活") is_public: bool = Field(False, description="是否公开") load_balance_strategy: Optional[str] = Field(LoadBalanceStrategy.NONE.value, description="负载均衡策略") + capability: List[str] = Field(default_factory=list, description="模型能力列表") + is_omni: bool = Field(False, description="是否为Omni模型") class ApiKeyCreateNested(BaseModel): @@ -30,6 +32,8 @@ class ApiKeyCreateNested(BaseModel): provider: Optional[str] = Field(None, description="API Key提供商") api_key: str = Field(..., description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) + capability: Optional[List[str]] = Field(None, description="模型能力列表") + is_omni: Optional[bool] = Field(None, description="是否为Omni模型") config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置") priority: str = Field("1", description="优先级", max_length=10) @@ -63,6 +67,8 @@ class ModelConfigUpdate(BaseModel): config: Optional[Dict[str, Any]] = Field(None, description="模型配置参数") is_active: Optional[bool] = Field(None, description="是否激活") is_public: Optional[bool] = Field(None, description="是否公开") + capability: Optional[List[str]] = Field(None, description="模型能力列表") + is_omni: Optional[bool] = Field(None, description="是否为Omni模型") class ModelConfig(ModelConfigBase): @@ -95,6 +101,8 @@ class ModelApiKeyCreateByProvider(BaseModel): api_key: str = Field(..., description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) description: Optional[str] = Field(None, description="备注") + capability: Optional[List[str]] = Field(None, description="模型能力列表") + is_omni: Optional[bool] = Field(None, description="是否为Omni模型") config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置") is_active: bool = Field(True, description="是否激活") priority: str = Field("1", description="优先级", max_length=10) @@ -108,6 +116,8 @@ class ModelApiKeyBase(BaseModel): provider: ModelProvider = Field(..., description="API Key提供商") api_key: str = Field(..., description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) + capability: List[str] = Field(default_factory=list, description="模型能力列表") + is_omni: bool = Field(False, description="是否为Omni模型") config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置") is_active: bool = Field(True, description="是否激活") priority: str = Field("1", description="优先级", max_length=10) @@ -124,6 +134,8 @@ class ModelApiKeyUpdate(BaseModel): provider: Optional[ModelProvider] = Field(None, description="API Key提供商") api_key: Optional[str] = Field(None, description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) + capability: Optional[List[str]] = Field(None, description="模型能力列表") + is_omni: Optional[bool] = Field(None, description="是否为Omni模型") config: Optional[Dict[str, Any]] = Field(None, description="API Key特定配置") is_active: Optional[bool] = Field(None, description="是否激活") priority: Optional[str] = Field(None, description="优先级", max_length=10) @@ -270,6 +282,8 @@ class ModelBaseCreate(BaseModel): description: Optional[str] = Field(None, description="模型描述") is_official: bool = Field(True, description="是否供应商官方模型") tags: List[str] = Field(default_factory=list, description="模型标签") + capability: List[str] = Field(default_factory=list, description="模型能力列表(如['vision', 'audio', 'video'])") + is_omni: bool = Field(False, description="是否为Omni模型") class ModelBaseUpdate(BaseModel): @@ -282,6 +296,8 @@ class ModelBaseUpdate(BaseModel): is_deprecated: Optional[bool] = Field(None, description="是否弃用") is_official: Optional[bool] = Field(None, description="是否供应商官方模型") tags: Optional[List[str]] = Field(None, description="模型标签") + capability: Optional[List[str]] = Field(None, description="模型能力列表") + is_omni: Optional[bool] = Field(None, description="是否为Omni模型") class ModelBase(BaseModel): @@ -298,6 +314,8 @@ class ModelBase(BaseModel): is_official: bool tags: List[str] add_count: int + capability: List[str] = [] + is_omni: bool = False class ModelBaseQuery(BaseModel): diff --git a/api/app/services/app_chat_service.py b/api/app/services/app_chat_service.py index 9723121d..e6ac227b 100644 --- a/api/app/services/app_chat_service.py +++ b/api/app/services/app_chat_service.py @@ -157,6 +157,7 @@ class AppChatService: api_key=api_key_obj.api_key, provider=api_key_obj.provider, api_base=api_key_obj.api_base, + is_omni=api_key_obj.is_omni, temperature=model_parameters.get("temperature", 0.7), max_tokens=model_parameters.get("max_tokens", 2000), system_prompt=system_prompt, @@ -180,7 +181,7 @@ class AppChatService: # 处理多模态文件 processed_files = None if files: - multimodal_service = MultimodalService(self.db) + multimodal_service = MultimodalService(self.db, api_key_obj.provider, is_omni=api_key_obj.is_omni) processed_files = await multimodal_service.process_files(files) logger.info(f"处理了 {len(processed_files)} 个文件") @@ -343,6 +344,7 @@ class AppChatService: api_key=api_key_obj.api_key, provider=api_key_obj.provider, api_base=api_key_obj.api_base, + is_omni=api_key_obj.is_omni, temperature=model_parameters.get("temperature", 0.7), max_tokens=model_parameters.get("max_tokens", 2000), system_prompt=system_prompt, @@ -366,7 +368,7 @@ class AppChatService: # 处理多模态文件 processed_files = None if files: - multimodal_service = MultimodalService(self.db) + multimodal_service = MultimodalService(self.db, api_key_obj.provider, is_omni=api_key_obj.is_omni) processed_files = await multimodal_service.process_files(files) logger.info(f"处理了 {len(processed_files)} 个文件") diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index 6e6e0ecb..c5919af9 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -232,7 +232,7 @@ class AppService: # 检查主 Agent 的模型配置 multi_agent_config.default_model_config_id = master_agent_release.default_model_config_id - model_api_key = ModelApiKeyService.get_a_api_key(self.db, multi_agent_config.default_model_config_id) + model_api_key = ModelApiKeyService.get_available_api_key(self.db, multi_agent_config.default_model_config_id) if not model_api_key: raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id)) diff --git a/api/app/services/audio_transcription_service.py b/api/app/services/audio_transcription_service.py new file mode 100644 index 00000000..11d13f38 --- /dev/null +++ b/api/app/services/audio_transcription_service.py @@ -0,0 +1,101 @@ +""" +音频转文本服务 + +支持的服务商: +- DashScope (阿里云通义千问) +- OpenAI Whisper +""" +import httpx + +from app.core.logging_config import get_business_logger + +logger = get_business_logger() + + +class AudioTranscriptionService: + """音频转文本服务""" + + @staticmethod + async def transcribe_dashscope(audio_url: str, api_key: str) -> str: + """ + 使用阿里云通义千问语音识别服务转换音频为文本 + + Args: + audio_url: 音频文件 URL + api_key: DashScope API Key + + Returns: + str: 转录的文本 + """ + try: + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + "https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-DashScope-Async": "enable", + }, + json={ + "model": "paraformer-v2", + "input": { + "file_urls": [audio_url] + }, + "parameters": { + "language_hints": ["zh", "en", "ja", "yue", "ko", "de", "fr", "ru"] + } + } + ) + response.raise_for_status() + result = response.json() + + if result.get("output", {}).get("results"): + text = result["output"]["results"][0].get("transcription_text", "") + logger.info(f"音频转文本成功: {len(text)} 字符") + return text + + return "[音频转文本失败]" + + except Exception as e: + logger.error(f"DashScope 音频转文本失败: {e}") + return f"[音频转文本失败: {str(e)}]" + + @staticmethod + async def transcribe_openai(audio_url: str, api_key: str) -> str: + """ + 使用 OpenAI Whisper 转换音频为文本 + + Args: + audio_url: 音频文件 URL + api_key: OpenAI API Key + + Returns: + str: 转录的文本 + """ + try: + # 下载音频文件 + async with httpx.AsyncClient(timeout=60.0) as client: + audio_response = await client.get(audio_url) + audio_response.raise_for_status() + audio_data = audio_response.content + + # 调用 Whisper API + files = {"file": ("audio.mp3", audio_data, "audio/mpeg")} + data = {"model": "whisper-1"} + + response = await client.post( + "https://api.openai.com/v1/audio/transcriptions", + headers={"Authorization": f"Bearer {api_key}"}, + files=files, + data=data + ) + response.raise_for_status() + result = response.json() + + text = result.get("text", "") + logger.info(f"音频转文本成功: {len(text)} 字符") + return text + + except Exception as e: + logger.error(f"OpenAI Whisper 音频转文本失败: {e}") + return f"[音频转文本失败: {str(e)}]" diff --git a/api/app/services/collaborative_orchestrator.py b/api/app/services/collaborative_orchestrator.py index 00a731de..68181cd1 100644 --- a/api/app/services/collaborative_orchestrator.py +++ b/api/app/services/collaborative_orchestrator.py @@ -445,6 +445,7 @@ class CollaborativeOrchestrator: "provider": api_key_config.provider, "api_key": api_key_config.api_key, "api_base": api_key_config.api_base, + "is_omni": api_key_config.is_omni, "model_parameters": config_data.get("model_parameters", {}), "api_key_id": api_key_config.id } @@ -511,6 +512,7 @@ class CollaborativeOrchestrator: provider=agent_config["provider"], api_key=agent_config["api_key"], base_url=agent_config.get("api_base"), + is_omni=agent_config.get("is_omni", False), extra_params=extra_params ) diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 8977710b..693f1a26 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -415,6 +415,7 @@ class DraftRunService: api_key=api_key_config["api_key"], provider=api_key_config.get("provider", "openai"), api_base=api_key_config.get("api_base"), + is_omni=api_key_config.get("is_omni", False), temperature=effective_params.get("temperature", 0.7), max_tokens=effective_params.get("max_tokens", 2000), system_prompt=system_prompt, @@ -442,7 +443,7 @@ class DraftRunService: if files: # 获取 provider 信息 provider = api_key_config.get("provider", "openai") - multimodal_service = MultimodalService(self.db, provider=provider) + multimodal_service = MultimodalService(self.db, provider=provider, is_omni=api_key_config.get("is_omni", False)) processed_files = await multimodal_service.process_files(files) logger.info(f"处理了 {len(processed_files)} 个文件,provider={provider}") @@ -683,6 +684,7 @@ class DraftRunService: api_key=api_key_config["api_key"], provider=api_key_config.get("provider", "openai"), api_base=api_key_config.get("api_base"), + is_omni=api_key_config.get("is_omni", False), temperature=effective_params.get("temperature", 0.7), max_tokens=effective_params.get("max_tokens", 2000), system_prompt=system_prompt, @@ -711,7 +713,7 @@ class DraftRunService: if files: # 获取 provider 信息 provider = api_key_config.get("provider", "openai") - multimodal_service = MultimodalService(self.db, provider=provider) + multimodal_service = MultimodalService(self.db, provider=provider, is_omni=api_key_config.get("is_omni", False)) processed_files = await multimodal_service.process_files(files) logger.info(f"处理了 {len(processed_files)} 个文件,provider={provider}") @@ -809,7 +811,7 @@ class DraftRunService: """ return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" - async def _get_api_key(self, model_config_id: uuid.UUID) -> Dict[str, str]: + async def _get_api_key(self, model_config_id: uuid.UUID) -> Dict: """获取模型的 API Key Args: @@ -846,7 +848,8 @@ class DraftRunService: "provider": api_key.provider, "api_key": api_key.api_key, "api_base": api_key.api_base, - "api_key_id": api_key.id + "api_key_id": api_key.id, + "is_omni": api_key.is_omni } async def _ensure_conversation( diff --git a/api/app/services/handoffs_service.py b/api/app/services/handoffs_service.py index e490eea4..8418fe31 100644 --- a/api/app/services/handoffs_service.py +++ b/api/app/services/handoffs_service.py @@ -544,6 +544,7 @@ def convert_multi_agent_config_to_handoffs( provider=model_api_key.provider, api_key=model_api_key.api_key, base_url=model_api_key.api_base, + is_omni=model_api_key.is_omni, extra_params={ "temperature": 0.7, "max_tokens": 2000, diff --git a/api/app/services/llm_router.py b/api/app/services/llm_router.py index e56ad5aa..02895d6b 100644 --- a/api/app/services/llm_router.py +++ b/api/app/services/llm_router.py @@ -414,6 +414,7 @@ class LLMRouter: provider=api_key_config.provider, api_key=api_key_config.api_key, base_url=api_key_config.api_base, + is_omni=api_key_config.is_omni, temperature=0.3, max_tokens=500 ) diff --git a/api/app/services/master_agent_router.py b/api/app/services/master_agent_router.py index 3cf3ecc3..b0f43b51 100644 --- a/api/app/services/master_agent_router.py +++ b/api/app/services/master_agent_router.py @@ -392,6 +392,7 @@ class MasterAgentRouter: provider=api_key_config.provider, api_key=api_key_config.api_key, base_url=api_key_config.api_base, + is_omni=api_key_config.is_omni, extra_params = extra_params ) diff --git a/api/app/services/model_service.py b/api/app/services/model_service.py index aa8cfbac..2337427a 100644 --- a/api/app/services/model_service.py +++ b/api/app/services/model_service.py @@ -90,7 +90,8 @@ class ModelConfigService: api_key: str, api_base: Optional[str] = None, model_type: str = "llm", - test_message: str = "Hello" + test_message: str = "Hello", + is_omni: bool = False ) -> Dict[str, Any]: """验证模型配置是否有效 @@ -102,6 +103,7 @@ class ModelConfigService: api_base: API基础URL model_type: 模型类型 (llm/chat/embedding/rerank) test_message: 测试消息 + is_omni: 是否为Omni模型 Returns: Dict: 验证结果 @@ -114,14 +116,27 @@ class ModelConfigService: try: start_time = time.time() - model_config = RedBearModelConfig( - model_name=model_name, - provider=provider, - api_key=api_key, - base_url=api_base, - temperature=0.7, - max_tokens=100 - ) + # dashscope 的 omni 模型需要使用 compatible-mode + if provider.lower() == ModelProvider.DASHSCOPE and is_omni: + if not api_base: + api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1" + model_config = RedBearModelConfig( + model_name=model_name, + provider=ModelProvider.OPENAI, + api_key=api_key, + base_url=api_base, + temperature=0.7, + max_tokens=100 + ) + else: + model_config = RedBearModelConfig( + model_name=model_name, + provider=provider, + api_key=api_key, + base_url=api_base, + temperature=0.7, + max_tokens=100 + ) # 根据模型类型选择不同的验证方式 model_type_lower = model_type.lower() @@ -257,8 +272,9 @@ class ModelConfigService: provider=model_data.provider, api_key=api_key_data.api_key, api_base=api_key_data.api_base, - model_type=model_data.type, # 传递模型类型 - test_message="Hello" + model_type=model_data.type, + test_message="Hello", + is_omni=model_data.is_omni ) if not validation_result["valid"]: raise BusinessException( @@ -279,6 +295,9 @@ class ModelConfigService: for api_key_data in api_key_datas: api_key_data.model_name = model_data.name api_key_data.provider = model_data.provider + # 同步capability和is_omni + api_key_data.capability = model_data.capability + api_key_data.is_omni = model_data.is_omni api_key_create_schema = ModelApiKeyCreate( model_config_ids=[model.id], **api_key_data.model_dump() @@ -497,6 +516,8 @@ class ModelApiKeyService: existing_key.config = data.config existing_key.priority = data.priority existing_key.model_name = model_name + existing_key.capability = data.capability + existing_key.is_omni = data.is_omni # 检查是否已关联该模型配置 if model_config not in existing_key.model_configs: @@ -513,7 +534,8 @@ class ModelApiKeyService: api_key=data.api_key, api_base=data.api_base, model_type=model_config.type, - test_message="Hello" + test_message="Hello", + is_omni=data.is_omni ) if not validation_result["valid"]: # 记录验证失败的模型,但不抛出异常 @@ -528,6 +550,8 @@ class ModelApiKeyService: provider=data.provider, api_key=data.api_key, api_base=data.api_base, + capability=data.capability if data.capability is not None else model_config.capability, + is_omni=data.is_omni if data.is_omni is not None else model_config.is_omni, config=data.config, is_active=data.is_active, priority=data.priority @@ -572,6 +596,8 @@ class ModelApiKeyService: existing_key.config = api_key_data.config existing_key.priority = api_key_data.priority existing_key.model_name = api_key_data.model_name + existing_key.capability = api_key_data.capability + existing_key.is_omni = api_key_data.is_omni # 检查是否已关联该模型配置 if model_config not in existing_key.model_configs: @@ -589,7 +615,8 @@ class ModelApiKeyService: api_key=api_key_data.api_key, api_base=api_key_data.api_base, model_type=model_config.type, - test_message="Hello" + test_message="Hello", + is_omni=model_config.is_omni ) if not validation_result["valid"]: raise BusinessException( @@ -620,7 +647,8 @@ class ModelApiKeyService: api_key=api_key_data.api_key or existing_api_key.api_key, api_base=api_key_data.api_base or existing_api_key.api_base, model_type=model_config.type, - test_message="Hello" + test_message="Hello", + is_omni=model_config.is_omni ) if not validation_result["valid"]: raise BusinessException( @@ -755,6 +783,8 @@ class ModelBaseService: "type": model_base.type, "logo": model_base.logo, "description": model_base.description, + "capability": model_base.capability, + "is_omni": model_base.is_omni, "is_composite": False } model_config = ModelConfigRepository.create(db, model_config_data) diff --git a/api/app/services/multi_agent_orchestrator.py b/api/app/services/multi_agent_orchestrator.py index d1aa46d1..650f639b 100644 --- a/api/app/services/multi_agent_orchestrator.py +++ b/api/app/services/multi_agent_orchestrator.py @@ -2593,6 +2593,7 @@ class MultiAgentOrchestrator: provider=api_key_config.provider, api_key=api_key_config.api_key, base_url=api_key_config.api_base, + is_omni=api_key_config.is_omni, temperature=0.7, # 整合任务使用中等温度 max_tokens=2000 ) @@ -2758,6 +2759,7 @@ class MultiAgentOrchestrator: provider=api_key_config.provider, api_key=api_key_config.api_key, base_url=api_key_config.api_base, + is_omni=api_key_config.is_omni, temperature=0.7, max_tokens=2000, extra_params={"streaming": True} # 启用流式输出 diff --git a/api/app/services/multi_agent_service.py b/api/app/services/multi_agent_service.py index c52814ed..751099d5 100644 --- a/api/app/services/multi_agent_service.py +++ b/api/app/services/multi_agent_service.py @@ -267,7 +267,7 @@ class MultiAgentService: # 2. 验证模型配置(如果提供了) if data.default_model_config_id: - model_api_key = ModelApiKeyService.get_a_api_key(self.db, data.default_model_config_id) + model_api_key = ModelApiKeyService.get_available_api_key(self.db, data.default_model_config_id) if not model_api_key: raise ResourceNotFoundException("模型配置", str(data.default_model_config_id)) diff --git a/api/app/services/multimodal_service.py b/api/app/services/multimodal_service.py index bfb23a56..9b06c287 100644 --- a/api/app/services/multimodal_service.py +++ b/api/app/services/multimodal_service.py @@ -9,47 +9,100 @@ - OpenAI: 支持 URL 和 base64 格式 """ import uuid -from typing import List, Dict, Any, Optional, Protocol +import httpx +import base64 +from typing import List, Dict, Any, Optional +from abc import ABC, abstractmethod from sqlalchemy.orm import Session +from docx import Document +import io +import PyPDF2 from app.core.logging_config import get_business_logger from app.core.exceptions import BusinessException from app.core.error_codes import BizCode from app.schemas.app_schema import FileInput, FileType, TransferMethod -from app.models.generic_file_model import GenericFile +from app.models.file_metadata_model import FileMetadata +from app.core.config import settings +from app.services.audio_transcription_service import AudioTranscriptionService logger = get_business_logger() -class ImageFormatStrategy(Protocol): - """图片格式策略接口""" +class MultimodalFormatStrategy(ABC): + """多模态格式策略基类""" + + @abstractmethod + async def format_image(self, url: str) -> Dict[str, Any]: + """格式化图片""" + pass + + @abstractmethod + async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + """格式化文档""" + pass + + @abstractmethod + async def format_audio(self, file_type: str, url: str) -> Dict[str, Any]: + """格式化音频""" + pass + + @abstractmethod + async def format_video(self, url: str) -> Dict[str, Any]: + """格式化视频""" + pass + + +class DashScopeFormatStrategy(MultimodalFormatStrategy): + """通义千问策略""" async def format_image(self, url: str) -> Dict[str, Any]: - """将图片 URL 转换为特定 provider 的格式""" - ... - - -class DashScopeImageStrategy: - """通义千问图片格式策略""" - - async def format_image(self, url: str) -> Dict[str, Any]: - """通义千问格式: {"type": "image", "image": "url"}""" + """通义千问图片格式:{"type": "image", "image": "url"}""" return { "type": "image", "image": url } + async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + """通义千问文档格式""" + return { + "type": "text", + "text": f"\n{text}\n" + } -class BedrockImageStrategy: - """Bedrock/Anthropic 图片格式策略""" + async def format_audio(self, file_type: str, url: str, transcription: Optional[str] = None) -> Dict[str, Any]: + """ + 通义千问音频格式 + - 原生支持: qwen-audio 系列 + - 其他模型: 需要转录为文本 + """ + if transcription: + return { + "type": "text", + "text": f"" + } + # 通义千问音频格式:{"type": "audio", "audio": "url"} + return { + "type": "audio", + "audio": url + } + + async def format_video(self, url: str) -> Dict[str, Any]: + """通义千问视频格式(qwen-vl 系列原生支持)""" + return { + "type": "video", + "video": url + } + + +class BedrockFormatStrategy(MultimodalFormatStrategy): + """Bedrock/Anthropic 策略""" async def format_image(self, url: str) -> Dict[str, Any]: """ Bedrock/Anthropic 格式: base64 编码 {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} """ - import httpx - import base64 from mimetypes import guess_type logger.info(f"下载并编码图片: {url}") @@ -84,9 +137,46 @@ class BedrockImageStrategy: } } + async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + """Bedrock/Anthropic 文档格式(需要 base64 编码)""" + # Bedrock 文档需要 base64 编码 + text_bytes = text.encode('utf-8') + base64_text = base64.b64encode(text_bytes).decode('utf-8') -class OpenAIImageStrategy: - """OpenAI 图片格式策略""" + return { + "type": "document", + "source": { + "type": "base64", + "media_type": "text/plain", + "data": base64_text + } + } + + async def format_audio(self, file_type: str, url: str, transcription: Optional[str] = None) -> Dict[str, Any]: + """ + Bedrock/Anthropic 音频格式 + 不支持原生音频,必须转录为文本 + """ + if transcription: + return { + "type": "text", + "text": f"[音频转录]\n{transcription}" + } + return { + "type": "text", + "text": "[音频文件:Bedrock 不支持原生音频,请启用音频转文本功能]" + } + + async def format_video(self, url: str) -> Dict[str, Any]: + """Bedrock/Anthropic 视频格式""" + return { + "type": "text", + "text": f"" + } + + +class OpenAIFormatStrategy(MultimodalFormatStrategy): + """OpenAI 策略""" async def format_image(self, url: str) -> Dict[str, Any]: """OpenAI 格式: {"type": "image_url", "image_url": {"url": "..."}}""" @@ -97,29 +187,97 @@ class OpenAIImageStrategy: } } + async def format_document(self, file_name: str, text: str) -> Dict[str, Any]: + """OpenAI 文档格式""" + return { + "type": "text", + "text": f"\n{text}\n" + } + + async def format_audio(self, file_type: str, url: str, transcription: Optional[str] = None) -> Dict[str, Any]: + """ + OpenAI 音频格式 + - gpt-4o-audio 系列支持原生音频(需要 base64 编码) + - 其他模型使用转录文本 + """ + if transcription: + return { + "type": "text", + "text": f"" + } + + # OpenAI 音频需要 base64 编码 + try: + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(url) + response.raise_for_status() + audio_data = response.content + base64_audio = base64.b64encode(audio_data).decode('utf-8') + # 1. 优先从 file_type (MIME) 取扩展名 + file_ext = file_type.split('/')[-1] if file_type and '/' in file_type else None + # 2. 从响应头 content-type 取 + if not file_ext: + ct = response.headers.get("content-type", "") + file_ext = ct.split('/')[-1].split(';')[0].strip() if '/' in ct else None + # 3. 从 URL 路径取扩展名 + if not file_ext: + file_ext = url.split('?')[0].rsplit('.', 1)[-1].lower() or None + # 4. 默认 wav + # supported_ext = {"wav", "mp3", "mp4", "ogg", "flac", "webm", "m4a", "wave", "x-m4a"} + file_ext = "wav" if not file_ext else file_ext + + return { + "type": "input_audio", + "input_audio": { + "data": f"data:;base64,{base64_audio}", + "format": file_ext + } + } + except Exception as e: + logger.error(f"下载音频失败: {e}") + return { + "type": "text", + "text": f"[音频处理失败: {str(e)}]" + } + + async def format_video(self, url: str) -> Dict[str, Any]: + """OpenAI 视频格式""" + return { + "type": "video_url", + "video_url": { + "url": url + } + } + # Provider 到策略的映射 PROVIDER_STRATEGIES = { - "dashscope": DashScopeImageStrategy, - "bedrock": BedrockImageStrategy, - "anthropic": BedrockImageStrategy, - "openai": OpenAIImageStrategy, + "dashscope": DashScopeFormatStrategy, + "bedrock": BedrockFormatStrategy, + "anthropic": BedrockFormatStrategy, + "openai": OpenAIFormatStrategy, } class MultimodalService: """多模态文件处理服务""" - def __init__(self, db: Session, provider: str = "dashscope"): + def __init__(self, db: Session, provider: str = "dashscope", api_key: Optional[str] = None, enable_audio_transcription: bool = False, is_omni: bool = False): """ 初始化多模态服务 Args: db: 数据库会话 - provider: 模型提供商(dashscope, bedrock, anthropic 等) + provider: 模型提供商(dashscope, bedrock, anthropic, openai 等) + api_key: API 密钥(用于音频转文本) + enable_audio_transcription: 是否启用音频转文本 + is_omni: 是否为 Omni 模型(dashscope 的 omni 模型需要使用 OpenAI 兼容格式) """ self.db = db self.provider = provider.lower() + self.api_key = api_key + self.enable_audio_transcription = enable_audio_transcription + self.is_omni = is_omni async def process_files( self, @@ -137,20 +295,32 @@ class MultimodalService: if not files: return [] + # 获取对应的策略 + # dashscope 的 omni 模型使用 OpenAI 兼容格式 + if self.provider == "dashscope" and self.is_omni: + strategy_class = OpenAIFormatStrategy + else: + strategy_class = PROVIDER_STRATEGIES.get(self.provider) + if not strategy_class: + logger.warning(f"未找到 provider '{self.provider}' 的策略,使用默认策略") + strategy_class = DashScopeFormatStrategy + + strategy = strategy_class() + result = [] for idx, file in enumerate(files): try: if file.type == FileType.IMAGE: - content = await self._process_image(file) + content = await self._process_image(file, strategy) result.append(content) elif file.type == FileType.DOCUMENT: - content = await self._process_document(file) + content = await self._process_document(file, strategy) result.append(content) elif file.type == FileType.AUDIO: - content = await self._process_audio(file) + content = await self._process_audio(file, strategy) result.append(content) elif file.type == FileType.VIDEO: - content = await self._process_video(file) + content = await self._process_video(file, strategy) result.append(content) else: logger.warning(f"不支持的文件类型: {file.type}") @@ -172,55 +342,29 @@ class MultimodalService: logger.info(f"成功处理 {len(result)}/{len(files)} 个文件,provider={self.provider}") return result - async def _process_image(self, file: FileInput) -> Dict[str, Any]: + async def _process_image(self, file: FileInput, strategy) -> Dict[str, Any]: """ 处理图片文件 Args: file: 图片文件输入 + strategy: 格式化策略 Returns: - Dict: 根据 provider 返回不同格式 - - Anthropic/Bedrock: {"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - - 通义千问: {"type": "image", "image": "url"} + Dict: 根据 provider 返回不同格式的图片内容 """ - url = await self.get_file_url(file) - - logger.debug(f"处理图片: {url}, provider={self.provider}") - - # 根据 provider 返回不同格式 - if self.provider in ["bedrock", "anthropic"]: - # Anthropic/Bedrock 只支持 base64 格式,需要下载并转换 - try: - logger.info(f"开始下载并编码图片: {url}") - base64_data, media_type = await self._download_and_encode_image(url) - result = { - "type": "image", - "source": { - "type": "base64", - "media_type": media_type, - "data": base64_data[:100] + "..." # 只记录前100个字符 - } - } - logger.info(f"图片编码完成: media_type={media_type}, data_length={len(base64_data)}") - # 返回完整数据 - result["source"]["data"] = base64_data - return result - except Exception as e: - logger.error(f"下载并编码图片失败: {e}", exc_info=True) - # 返回错误提示 - return { - "type": "text", - "text": f"[图片加载失败: {str(e)}]" - } - else: - # 通义千问等其他格式支持 URL + try: + url = await self.get_file_url(file) + return await strategy.format_image(url) + except Exception as e: + logger.error(f"处理图片失败: {e}", exc_info=True) return { - "type": "image", - "image": url + "type": "text", + "text": f"[图片处理失败: {str(e)}]" } - async def _download_and_encode_image(self, url: str) -> tuple[str, str]: + @staticmethod + async def _download_and_encode_image(url: str) -> tuple[str, str]: """ 下载图片并转换为 base64 @@ -230,8 +374,6 @@ class MultimodalService: Returns: tuple: (base64_data, media_type) """ - import httpx - import base64 from mimetypes import guess_type # 下载图片 @@ -258,15 +400,16 @@ class MultimodalService: return base64_data, media_type - async def _process_document(self, file: FileInput) -> Dict[str, Any]: + async def _process_document(self, file: FileInput, strategy) -> Dict[str, Any]: """ 处理文档文件(PDF、Word 等) Args: file: 文档文件输入 + strategy: 格式化策略 Returns: - Dict: text 格式的内容(包含提取的文本) + Dict: 根据 provider 返回不同格式的文档内容 """ if file.transfer_method == TransferMethod.REMOTE_URL: # 远程文档暂不支持提取 @@ -277,48 +420,68 @@ class MultimodalService: else: # 本地文件,提取文本内容 text = await self._extract_document_text(file.upload_file_id) - generic_file = self.db.query(GenericFile).filter( - GenericFile.id == file.upload_file_id + file_metadata = self.db.query(FileMetadata).filter( + FileMetadata.id == file.upload_file_id ).first() - file_name = generic_file.file_name if generic_file else "unknown" + file_name = file_metadata.file_name if file_metadata else "unknown" - return { - "type": "text", - "text": f"\n{text}\n" - } + # 使用策略格式化文档 + return await strategy.format_document(file_name, text) - async def _process_audio(self, file: FileInput) -> Dict[str, Any]: + async def _process_audio(self, file: FileInput, strategy) -> Dict[str, Any]: """ 处理音频文件 Args: file: 音频文件输入 + strategy: 格式化策略 Returns: - Dict: 音频内容(暂时返回占位符) + Dict: 根据 provider 返回不同格式的音频内容 """ - # TODO: 实现音频转文字功能 - return { - "type": "text", - "text": "[音频文件,暂不支持处理]" - } + try: + url = await self.get_file_url(file) - async def _process_video(self, file: FileInput) -> Dict[str, Any]: + # 如果启用音频转文本且有 API Key + transcription = None + if self.enable_audio_transcription and self.api_key: + logger.info(f"开始音频转文本: {url}") + if self.provider == "dashscope": + transcription = await AudioTranscriptionService.transcribe_dashscope(url, self.api_key) + elif self.provider == "openai": + transcription = await AudioTranscriptionService.transcribe_openai(url, self.api_key) + else: + logger.warning(f"Provider {self.provider} 不支持音频转文本") + + return await strategy.format_audio(file.file_type, url, transcription) + except Exception as e: + logger.error(f"处理音频失败: {e}", exc_info=True) + return { + "type": "text", + "text": f"[音频处理失败: {str(e)}]" + } + + async def _process_video(self, file: FileInput, strategy) -> Dict[str, Any]: """ 处理视频文件 Args: file: 视频文件输入 + strategy: 格式化策略 Returns: - Dict: 视频内容(暂时返回占位符) + Dict: 根据 provider 返回不同格式的视频内容 """ - # TODO: 实现视频处理功能 - return { - "type": "text", - "text": "[视频文件,暂不支持处理]" - } + try: + url = await self.get_file_url(file) + return await strategy.format_video(url) + except Exception as e: + logger.error(f"处理视频失败: {e}", exc_info=True) + return { + "type": "text", + "text": f"[视频处理失败: {str(e)}]" + } async def get_file_url(self, file: FileInput) -> str: """ @@ -336,26 +499,22 @@ class MultimodalService: if file.transfer_method == TransferMethod.REMOTE_URL: return file.url else: - # 本地文件,通过 file_storage 系统获取永久访问 URL - from app.models.file_metadata_model import FileMetadata - from app.core.config import settings - file_id = file.upload_file_id print("="*50) print("file_id",file_id) - + # 查询 FileMetadata file_metadata = self.db.query(FileMetadata).filter( FileMetadata.id == file_id, FileMetadata.status == "completed" ).first() - + if not file_metadata: raise BusinessException( f"文件不存在或已删除: {file_id}", BizCode.NOT_FOUND ) - + # 返回永久URL server_url = settings.FILE_LOCAL_SERVER_URL return f"{server_url}/storage/permanent/{file_id}" @@ -370,58 +529,79 @@ class MultimodalService: Returns: str: 提取的文本内容 """ - generic_file = self.db.query(GenericFile).filter( - GenericFile.id == file_id, - GenericFile.status == "active" + file_metadata = self.db.query(FileMetadata).filter( + FileMetadata.id == file_id, + FileMetadata.status == "completed" ).first() - if not generic_file: + if not file_metadata: raise BusinessException( f"文件不存在或已删除: {file_id}", BizCode.NOT_FOUND ) - # TODO: 根据文件类型提取文本 - # - PDF: 使用 PyPDF2 或 pdfplumber - # - Word: 使用 python-docx - # - TXT/MD: 直接读取 - - file_ext = generic_file.file_ext.lower() + file_ext = file_metadata.file_ext.lower() + server_url = settings.FILE_LOCAL_SERVER_URL + file_url = f"{server_url}/storage/permanent/{file_id}" if file_ext in ['.txt', '.md', '.markdown']: - return await self._read_text_file(generic_file.storage_path) + return await self._read_text_file(file_url) elif file_ext == '.pdf': - return await self._extract_pdf_text(generic_file.storage_path) + return await self._extract_pdf_text(file_url) elif file_ext in ['.doc', '.docx']: - return await self._extract_word_text(generic_file.storage_path) + return await self._extract_word_text(file_url) else: return f"[不支持的文档格式: {file_ext}]" - async def _read_text_file(self, storage_path: str) -> str: + @staticmethod + async def _read_text_file(file_url: str) -> str: """读取纯文本文件""" try: - with open(storage_path, 'r', encoding='utf-8') as f: - return f.read() + # 下载文件 + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(file_url) + response.raise_for_status() + return response.text except Exception as e: logger.error(f"读取文本文件失败: {e}") return f"[文件读取失败: {str(e)}]" - async def _extract_pdf_text(self, storage_path: str) -> str: + @staticmethod + async def _extract_pdf_text(file_url: str) -> str: """提取 PDF 文本""" try: - # TODO: 实现 PDF 文本提取 - # import PyPDF2 或 pdfplumber - return "[PDF 文本提取功能待实现]" + # 下载 PDF 文件 + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(file_url) + response.raise_for_status() + pdf_data = response.content + + # 使用 BytesIO 读取 PDF + text_parts = [] + pdf_file = io.BytesIO(pdf_data) + pdf_reader = PyPDF2.PdfReader(pdf_file) + for page in pdf_reader.pages: + text_parts.append(page.extract_text()) + return '\n'.join(text_parts) except Exception as e: logger.error(f"提取 PDF 文本失败: {e}") return f"[PDF 提取失败: {str(e)}]" - async def _extract_word_text(self, storage_path: str) -> str: + @staticmethod + async def _extract_word_text(file_url: str) -> str: """提取 Word 文档文本""" try: - # TODO: 实现 Word 文本提取 - # import docx - return "[Word 文本提取功能待实现]" + # 下载 Word 文件 + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(file_url) + response.raise_for_status() + word_data = response.content + + # 使用 BytesIO 读取 Word 文档 + word_file = io.BytesIO(word_data) + doc = Document(word_file) + text_parts = [paragraph.text for paragraph in doc.paragraphs] + return '\n'.join(text_parts) except Exception as e: logger.error(f"提取 Word 文本失败: {e}") return f"[Word 提取失败: {str(e)}]" diff --git a/api/app/services/prompt_optimizer_service.py b/api/app/services/prompt_optimizer_service.py index 99edcc0e..184220a8 100644 --- a/api/app/services/prompt_optimizer_service.py +++ b/api/app/services/prompt_optimizer_service.py @@ -184,7 +184,8 @@ class PromptOptimizerService: model_name=api_config.model_name, provider=api_config.provider, api_key=api_config.api_key, - base_url=api_config.api_base + base_url=api_config.api_base, + is_omni=api_config.is_omni ), type=ModelType(model_config.type)) try: prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompt') diff --git a/api/app/services/shared_chat_service.py b/api/app/services/shared_chat_service.py index 89d3f3d6..0d659832 100644 --- a/api/app/services/shared_chat_service.py +++ b/api/app/services/shared_chat_service.py @@ -247,6 +247,7 @@ class SharedChatService: api_key=api_key_obj.api_key, provider=api_key_obj.provider, api_base=api_key_obj.api_base, + is_omni=api_key_obj.is_omni, temperature=model_parameters.get("temperature", 0.7), max_tokens=model_parameters.get("max_tokens", 2000), system_prompt=system_prompt, @@ -454,6 +455,7 @@ class SharedChatService: api_key=api_key_obj.api_key, provider=api_key_obj.provider, api_base=api_key_obj.api_base, + is_omni=api_key_obj.is_omni, temperature=model_parameters.get("temperature", 0.7), max_tokens=model_parameters.get("max_tokens", 2000), system_prompt=system_prompt, From cf571cf02ba75d0702aa8b603efc3a7db47c87e1 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 10:01:11 +0800 Subject: [PATCH 46/83] fix(web): use modal.warning replace modal.confirm --- .../UserMemoryDetail/components/Suggestions.tsx | 7 ++++--- .../UserMemoryDetail/pages/ImplicitDetail.tsx | 17 +++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/web/src/views/UserMemoryDetail/components/Suggestions.tsx b/web/src/views/UserMemoryDetail/components/Suggestions.tsx index c67c0d80..3b7c1800 100644 --- a/web/src/views/UserMemoryDetail/components/Suggestions.tsx +++ b/web/src/views/UserMemoryDetail/components/Suggestions.tsx @@ -4,7 +4,7 @@ * @Last Modified by: ZhaoYing * @Last Modified time: 2026-03-04 16:22:03 */ -import { useEffect, useState, forwardRef, useImperativeHandle } from 'react' +import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' import { App } from 'antd' @@ -43,9 +43,11 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }, { refresh: () => const { modal } = App.useApp() const [loading, setLoading] = useState(false) const [suggestions, setSuggestions] = useState(null) + const modalInstanceRef = useRef<{ destroy: () => void } | null>(null) useEffect(() => { getSuggestionData() + return () => modalInstanceRef.current?.destroy() }, [id]) const getSuggestionData = () => { @@ -57,10 +59,9 @@ const Suggestions = forwardRef<{ handleRefresh: () => void; }, { refresh: () => .then((res) => { const response = res as Suggestions if (!response.exists && (!response.suggestions || !response.suggestions?.length)) { - modal.confirm({ + modalInstanceRef.current = modal.warning({ title: t('statementDetail.noData'), okText: t('common.refresh'), - cancelText: t('common.cancel'), onOk: () => { refresh() } diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx index aa6f40c7..46286fff 100644 --- a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -36,19 +36,20 @@ const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }, { refresh: () // Check if implicit data exists, prompt user to initialize if not useEffect(() => { if (!id) return + let modalInstance: { destroy: () => void } | null = null implicitCheckData(id) .then(res => { if (!(res as { exists: boolean }).exists) { - modal.confirm({ - title: t('implicitDetail.noData'), - okText: t('common.refresh'), - cancelText: t('common.cancel'), - onOk: () => { - refresh() - } - }) + modalInstance = modal.warning({ + title: t('implicitDetail.noData'), + okText: t('common.refresh'), + onOk: () => { + refresh() + } + }) } }) + return () => modalInstance?.destroy() }, [id]) // Refresh all implicit memory components by regenerating profile From 0def474cc219bd3f2b84dbe0e9c6edca66d86b8c Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 10:30:35 +0800 Subject: [PATCH 47/83] feat(web): app's chat support audio/video/document file --- web/src/components/AudioRecorder/index.tsx | 21 +- web/src/components/Chat/ChatInput.tsx | 27 +- web/src/i18n/en.ts | 2 + web/src/i18n/zh.ts | 2 + web/src/utils/stream.ts | 14 +- .../ApplicationConfig/components/Chat.tsx | 260 ++++++++++-------- .../Conversation/components/FileUpload.tsx | 53 +++- .../components/UploadFileListModal.tsx | 6 +- web/src/views/Conversation/index.tsx | 41 ++- .../views/Workflow/components/Chat/Chat.tsx | 24 +- 10 files changed, 284 insertions(+), 166 deletions(-) diff --git a/web/src/components/AudioRecorder/index.tsx b/web/src/components/AudioRecorder/index.tsx index f6a030b4..d31746f6 100644 --- a/web/src/components/AudioRecorder/index.tsx +++ b/web/src/components/AudioRecorder/index.tsx @@ -1,16 +1,21 @@ import { type FC, useRef, useState } from 'react' import RecordRTC from 'recordrtc' -import { fileUpload } from '@/api/fileStorage' +import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' +import { request } from '@/utils/request' interface AudioRecorderProps { - onRecordingComplete?: (file: { file_id: string; file_key: string; }, blob: Blob) => void - className?: string + onRecordingComplete?: (file: { file_id: string; file_key: string; url: string; type?: string; }, blob?: Blob) => void + className?: string; + action?: string; + requestConfig?: Record; } const AudioRecorder: FC = ({ onRecordingComplete, className = '', + action = fileUploadUrlWithoutApiPrefix, + requestConfig = {} }) => { const [isRecording, setIsRecording] = useState(false) const recorderRef = useRef(null) @@ -33,11 +38,17 @@ const AudioRecorder: FC = ({ if (recorderRef.current) { recorderRef.current.stopRecording(() => { const blob = recorderRef.current!.getBlob() + const url = recorderRef.current!.toURL() const formData = new FormData() formData.append('file', blob, `recording_${Date.now()}.webm`) - fileUpload(formData) + request + .uploadFile(action, formData, requestConfig) .then(res => { - onRecordingComplete?.(res as { file_id: string; file_key: string; }, blob) + onRecordingComplete?.({ + ...(res as { file_id: string; file_key: string }), + type: blob.type, + url + }, blob) recorderRef.current?.destroy() recorderRef.current = null }) diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx index c155bb22..49fb65d2 100644 --- a/web/src/components/Chat/ChatInput.tsx +++ b/web/src/components/Chat/ChatInput.tsx @@ -2,10 +2,11 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:14 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-10 12:13:52 + * @Last Modified time: 2026-03-04 18:42:49 */ import { type FC, useEffect, useMemo } from 'react' import { Flex, Input, Form } from 'antd' + import SendIcon from '@/assets/images/conversation/send.svg' import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg' import LoadingIcon from '@/assets/images/conversation/loading.svg' @@ -80,9 +81,31 @@ const ChatInput: FC = ({
) } + if (file.type.includes('video')) { + return ( +
+
+ ) + } + if (file.type.includes('audio')) { + return ( +
+
+ ) + } return (
- {(file.type.includes('word') || file.type.includes('wordprocessingml.document')) &&
} {(file.type.includes('pdf')) &&
= ({ chatList, data, updateChatList, handleSave, sourc content: '', created_at: Date.now(), }; - + if (isCluster) { updateChatList(prev => prev.map(item => ({ ...item, @@ -134,7 +134,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc }) } /** Update assistant message when error occurs */ - const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => { + const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => { if (message_length > 0 || !model_config_id) return updateChatList(prev => { @@ -217,6 +217,8 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc } } if (!isCanSend) { + setLoading(false) + setCompareLoading(false) return } runCompare(data.app_id, { @@ -243,7 +245,15 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc "stream": true, "timeout": 60, }, handleStreamMessage) - .finally(() => setLoading(false)); + .catch(() => { + setLoading(false) + setCompareLoading(false) + updateClusterErrorAssistantMessage(0) + }) + .finally(() => { + setLoading(false) + setCompareLoading(false) + }) }, 0) }) .catch(() => { @@ -288,7 +298,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc }) } /** Update cluster message when error occurs */ - const updateClusterErrorAssistantMessage = (message_length: number) => { + const updateClusterErrorAssistantMessage = (message_length: number) => { if (message_length > 0) return updateChatList(prev => { @@ -331,7 +341,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc data.map(item => { const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number }; - switch(item.event) { + switch (item.event) { case 'start': if (conversation_id && conversationId !== conversation_id) { setConversationId(conversation_id); @@ -354,27 +364,35 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc }; setTimeout(() => { - draftRun( - data.app_id, - { - message, - conversation_id: conversationId, - stream: true, - files: fileList.map(file => { - if (file.url) { - return file - } else { - return { - type: file.type, - transfer_method: 'local_file', - upload_file_id: file.response.data.file_id - } + draftRun( + data.app_id, + { + message, + conversation_id: conversationId, + stream: true, + files: fileList.map(file => { + if (file.url) { + return file + } else { + return { + type: file.type, + transfer_method: 'local_file', + upload_file_id: file.response.data.file_id } - }), - }, - handleStreamMessage - ) - .finally(() => setLoading(false)) + } + }), + }, + handleStreamMessage + ) + .catch(() => { + setLoading(false) + setCompareLoading(false) + updateClusterErrorAssistantMessage(0) + }) + .finally(() => { + setLoading(false) + setCompareLoading(false) + }) }, 0) }) .catch(() => { @@ -393,12 +411,17 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc const fileChange = (file?: any) => { setFileList([...fileList, file]) } - // const handleRecordingComplete = async (file: any) => { - // console.log('file', file) - // } + const handleRecordingComplete = async (file: any) => { + setFileList([...fileList, { + uid: file.file_id, + response: { data: file }, + thumbUrl: file.url, + type: file.type + }]) + } const handleShowUpload: MenuProps['onClick'] = ({ key }) => { - switch(key) { + switch (key) { case 'define': uploadFileListModalRef.current?.handleOpen() break @@ -415,99 +438,98 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc return (
{chatList.length === 0 - ? - : <> -
- {chatList.map((chat, index) => ( -
1, - })}> - {chat.label && -
-
-
{chat.label}
-
handleDelete(index)} - >
+ : <> +
+ {chatList.map((chat, index) => ( +
1, + })}> + {chat.label && +
+
+
{chat.label}
+
handleDelete(index)} + >
+
-
- } - } - data={chat.list || []} - streamLoading={compareLoading} - labelPosition="top" - labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label} - errorDesc={t('application.ReplyException')} - /> -
- ))} -
-
- - - - - ) - }, - ], - onClick: handleShowUpload + } + -
-
+ contentClassNames={{ + 'rb:max-w-[400px]!': chatList.length === 1, + 'rb:max-w-[260px]!': chatList.length === 2, + 'rb:max-w-[150px]!': chatList.length === 3, + 'rb:max-w-[108px]!': chatList.length === 4, + }} + empty={} + data={chat.list || []} + streamLoading={compareLoading} + labelPosition="top" + labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label} + errorDesc={t('application.ReplyException')} + /> +
+ ))} +
+
+ + + + + ) + }, + ], + onClick: handleShowUpload + }} + > +
+
+
+ + + +
- {/* - - - */} - -
-
- + +
+ } { /** Custom file removal callback */ onRemove?: (file: UploadFile) => boolean | void | Promise; } + +const transform_file_type = { + 'text/plain': 'document/text', + 'text/markdown': 'document/markdown', + 'text/x-markdown': 'document/x-markdown', + + 'application/pdf': 'document/pdf', + + 'application/msword': 'document/doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document/docx', + + 'application/vnd.ms-powerpoint': 'document/ppt', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'document/pptx', +} // Mapping of file extensions to MIME types const ALL_FILE_TYPE: { [key: string]: string; } = { - // txt: 'text/plain', + txt: 'text/plain', + md: 'text/markdown', + xmd: 'text/x-markdown', + pdf: 'application/pdf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - - xls: 'application/vnd.ms-excel', - xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - csv: 'text/csv', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - - // md: 'text/markdown', - // htm: 'text/html', - // html: 'text/html', - // json: 'application/json', + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', @@ -84,6 +94,23 @@ const ALL_FILE_TYPE: { bmp: 'image/bmp', webp: 'image/webp', svg: 'image/svg+xml', + + mp4: 'video/mp4', + mov: 'video/quicktime', + avi: 'video/x-msvideo', + mkv: 'video/x-matroska', + webm: 'video/webm', + flv: 'video/x-flv', + wmv: 'video/x-ms-wmv', + + mp3: 'audio/mpeg', + wav: 'audio/wav', + ogg: 'audio/ogg', + aac: 'audio/aac', + flac: 'audio/flac', + m4a: 'audio/mp4', + wma: 'audio/x-ms-wma', + xm4a: 'audio/x-m4a', } export interface UploadFilesRef { /** Current file list */ @@ -178,6 +205,10 @@ const UploadFiles = forwardRef(({ * Handles upload state changes */ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { + newFileList.map(file => { + const type = (file.type && transform_file_type[file.type as keyof typeof transform_file_type]) || file.type + file.type = type + }) setFileList(newFileList); if (onChange) { onChange(maxCount === 1 ? newFileList[newFileList.length - 1] : newFileList); diff --git a/web/src/views/Conversation/components/UploadFileListModal.tsx b/web/src/views/Conversation/components/UploadFileListModal.tsx index c5110701..a43b9dd4 100644 --- a/web/src/views/Conversation/components/UploadFileListModal.tsx +++ b/web/src/views/Conversation/components/UploadFileListModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:47 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 10:17:54 + * @Last Modified time: 2026-03-04 17:47:09 */ /** * Upload File List Modal Component @@ -104,7 +104,9 @@ const UploadFileListModal = forwardRef diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index f532ac53..5509ad0a 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -14,7 +14,7 @@ import { type FC, useState, useEffect, useRef } from 'react' import { useParams, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import InfiniteScroll from 'react-infinite-scroll-component'; -import { Flex, Skeleton, Form, Dropdown, type MenuProps, App } from 'antd' +import { Flex, Skeleton, Form, Dropdown, type MenuProps, App, Divider } from 'antd' import { SettingOutlined } from '@ant-design/icons' import clsx from 'clsx' import dayjs from 'dayjs' @@ -35,7 +35,7 @@ import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg' import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg' import { type SSEMessage } from '@/utils/stream' import UploadFiles from './components/FileUpload' -// import AudioRecorder from '@/components/AudioRecorder' +import AudioRecorder from '@/components/AudioRecorder' import { shareFileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' import UploadFileListModal from './components/UploadFileListModal' import type { VariableConfigModalRef } from '@/views/Workflow/types' @@ -305,17 +305,27 @@ const Conversation: FC = () => { }), variables: params }, handleStreamMessage, shareToken) + .catch(() => { + setLoading(false) + setStreamLoading(false) + }) .finally(() => { setLoading(false) + setStreamLoading(false) }) } const fileChange = (file?: any) => { form.setFieldValue('files', [...(queryValues.files || []), file]) } - // const handleRecordingComplete = async (file: any) => { - // console.log('file', file) - // } + const handleRecordingComplete = async (file: any) => { + form.setFieldValue('files', [...(queryValues.files || []), { + uid: file.file_id, + response: { data: file }, + thumbUrl: file.url, + type: file.type + }]) + } const handleShowUpload: MenuProps['onClick'] = ({ key }) => { switch(key) { @@ -329,6 +339,7 @@ const Conversation: FC = () => { form.setFieldValue('files', [...(queryValues.files || []), ...fileList]) } const updateFileList = (fileList?: any[]) => { + console.log('fileList', fileList) form.setFieldValue('files', [...(fileList || [])]) } @@ -383,7 +394,7 @@ const Conversation: FC = () => {
} - contentClassName="rb:h-[calc(100%-180px)]" + contentClassName={!queryValues?.files?.length ? "rb:h-[calc(100%-144px)]" : "rb:h-[calc(100%-208px)]"} data={chatList} streamLoading={streamLoading} loading={loading} @@ -405,13 +416,12 @@ const Conversation: FC = () => { key: 'upload', label: ( ) }, @@ -455,10 +465,19 @@ const Conversation: FC = () => { )} - {/* - + + - */} + diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx index 65989b30..f8049cb7 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-28 16:43:06 + * @Last Modified time: 2026-03-04 18:51:48 */ /** * Workflow Chat Component @@ -23,7 +23,7 @@ */ import { forwardRef, useImperativeHandle, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { App, Space, Button, Flex, Dropdown, type MenuProps } from 'antd' +import { App, Space, Button, Flex, Dropdown, type MenuProps, Divider } from 'antd' import ChatIcon from '@/assets/images/application/chat.png' import RbDrawer from '@/components/RbDrawer'; @@ -38,7 +38,7 @@ import { type SSEMessage } from '@/utils/stream' import type { Variable } from '../Properties/VariableList/types' import ChatInput from '@/components/Chat/ChatInput' import UploadFiles from '@/views/Conversation/components/FileUpload' -// import AudioRecorder from '@/components/AudioRecorder' +import AudioRecorder from '@/components/AudioRecorder' import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' import type { UploadFileListModalRef } from '@/views/Conversation/types' import Runtime from './Runtime'; @@ -359,6 +359,7 @@ const Chat = forwardRef(({ appId setStreamLoading(true) draftRun(appId, data, handleStreamMessage) .catch((error) => { + console.log('draftRun error', error) setChatList(prev => { const newList = [...prev] const lastIndex = newList.length - 1 @@ -390,9 +391,13 @@ const Chat = forwardRef(({ appId const fileChange = (file?: any) => { setFileList([...fileList, file]) } - // const handleRecordingComplete = async (file: any) => { - // console.log('file', file) - // } + const handleRecordingComplete = async (file: any) => { + setFileList([...fileList, { + response: { data: file }, + thumbUrl: file.url, + type: file.type + }]) + } /** * Handles dropdown menu actions for file upload @@ -424,6 +429,8 @@ const Chat = forwardRef(({ appId handleClose })); + console.log('fileList', fileList) + return ( @@ -470,7 +477,6 @@ const Chat = forwardRef(({ appId { key: 'upload', label: ( ) @@ -484,10 +490,10 @@ const Chat = forwardRef(({ appId >
- {/* + - */} +
From 2bd364eca30e6bf60f00a7473014b78870777e1a Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 5 Mar 2026 10:46:31 +0800 Subject: [PATCH 48/83] [add] migration script --- .../versions/b4af97639217_202603051033.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 api/migrations/versions/b4af97639217_202603051033.py diff --git a/api/migrations/versions/b4af97639217_202603051033.py b/api/migrations/versions/b4af97639217_202603051033.py new file mode 100644 index 00000000..ddeae41c --- /dev/null +++ b/api/migrations/versions/b4af97639217_202603051033.py @@ -0,0 +1,63 @@ +"""202603051033 + +Revision ID: b4af97639217 +Revises: 4bf27c66ae63 +Create Date: 2026-03-05 10:36:06.282227 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b4af97639217' +down_revision: Union[str, None] = '4bf27c66ae63' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Add columns as nullable first to avoid table locks + op.add_column('model_api_keys', sa.Column('capability', sa.ARRAY(sa.String()), nullable=True, comment="模型能力列表(如['vision', 'audio', 'video'])")) + op.add_column('model_api_keys', sa.Column('is_omni', sa.Boolean(), nullable=True, comment='是否为Omni模型(使用特殊API调用)')) + + op.add_column('model_bases', sa.Column('capability', sa.ARRAY(sa.String()), nullable=True, comment="模型能力列表(如['vision', 'audio', 'video'])")) + op.add_column('model_bases', sa.Column('is_omni', sa.Boolean(), nullable=True, comment='是否为Omni模型(使用特殊API调用)')) + + op.add_column('model_configs', sa.Column('capability', sa.ARRAY(sa.String()), nullable=True, comment="模型能力列表(如['vision', 'audio', 'video'])")) + op.add_column('model_configs', sa.Column('is_omni', sa.Boolean(), nullable=True, comment='是否为Omni模型(使用特殊API调用)')) + + # Update existing rows with default values + op.execute("UPDATE model_api_keys SET capability = '{}' WHERE capability IS NULL") + op.execute("UPDATE model_api_keys SET is_omni = false WHERE is_omni IS NULL") + + op.execute("UPDATE model_bases SET capability = '{}' WHERE capability IS NULL") + op.execute("UPDATE model_bases SET is_omni = false WHERE is_omni IS NULL") + + op.execute("UPDATE model_configs SET capability = '{}' WHERE capability IS NULL") + op.execute("UPDATE model_configs SET is_omni = false WHERE is_omni IS NULL") + + # Now make columns NOT NULL + op.alter_column('model_api_keys', 'capability', nullable=False) + op.alter_column('model_api_keys', 'is_omni', nullable=False) + + op.alter_column('model_bases', 'capability', nullable=False) + op.alter_column('model_bases', 'is_omni', nullable=False) + + op.alter_column('model_configs', 'capability', nullable=False) + op.alter_column('model_configs', 'is_omni', nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('model_configs', 'is_omni') + op.drop_column('model_configs', 'capability') + op.drop_column('model_bases', 'is_omni') + op.drop_column('model_bases', 'capability') + op.drop_column('model_api_keys', 'is_omni') + op.drop_column('model_api_keys', 'capability') + # ### end Alembic commands ### From 1b666638bc94f0a775fc9b6748854c4fdaedfafe Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 10:58:25 +0800 Subject: [PATCH 49/83] feat(web): add SYSTEM_DEFAULT_SCENE_CANNOT_DELETE error i18n --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/utils/request.ts | 4 +++- web/src/views/Ontology/index.tsx | 12 +++++++----- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 02add0ec..7cef2d6c 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -440,6 +440,7 @@ export const en = { logoutApiCannotRefreshToken: 'Logout API cannot refresh token', publicApiCannotRefreshToken: 'Public API cannot refresh token', refreshTokenNotExist: 'Refresh token does not exist', + SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: 'This is a system preset scene and cannot be deleted', reset: 'Reset', refresh: 'Refresh', return: 'Return', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 06abf63a..5c688934 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1016,6 +1016,7 @@ export const zh = { logoutApiCannotRefreshToken: '退出登录接口不能刷新token', publicApiCannotRefreshToken: '公共接口不能刷新token', refreshTokenNotExist: '刷新token不存在', + SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: '该场景为系统预设场景,不允许删除', reset: '重置', refresh: '刷新', return: '返回', diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 3c3e8fa2..f58f5f65 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -183,7 +183,9 @@ service.interceptors.response.use( msg = msg || i18n.t('common.serverError'); break; default: - if (!msg && Array.isArray(error.response?.data?.detail)) { + if (msg === 'SYSTEM_DEFAULT_SCENE_CANNOT_DELETE') { + msg = i18n.t(`common.${msg}`) + } else if (!msg && Array.isArray(error.response?.data?.detail)) { msg = error.response?.data?.detail?.map((item: { msg: string }) => item.msg).join(';') } else { msg = msg || i18n.t('common.unknownError'); diff --git a/web/src/views/Ontology/index.tsx b/web/src/views/Ontology/index.tsx index 42a6544f..37f9118d 100644 --- a/web/src/views/Ontology/index.tsx +++ b/web/src/views/Ontology/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 14:10:15 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 14:10:15 + * @Last Modified time: 2026-03-05 10:57:53 */ import { type FC, useState, useRef, type MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -164,11 +164,13 @@ const Ontology: FC = () => {
))} - +
{t('ontology.entityTypes')}:
- {item.entity_type?.map((type, i) => ( - {type} - ))} +
+ {item.entity_type?.map((type, i) => ( + {type} + ))} +
{item.type_num > 3 && ( +{item.type_num - 3} )} From b5ba53208e41fa3c265e859b92bb1ccb0401b102 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 11:05:51 +0800 Subject: [PATCH 50/83] feat(web): chat variable support paragraph --- web/src/views/Workflow/components/Chat/VariableConfigModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx index 5acd3eb1..66491ab7 100644 --- a/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx +++ b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx @@ -80,6 +80,7 @@ const VariableConfigModal = forwardRef } + { field.type === 'paragraph' && } { field.type === 'number' && form.setFieldValue(['variables', name, 'value'], value)} /> } From e511b149330ca42dd36dc1b8cbfc1ecc463651f2 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Thu, 5 Mar 2026 11:06:46 +0800 Subject: [PATCH 51/83] [fix] Deleting the default scene results in a 400 status code. A unified language pop-up prompt is displayed. --- api/app/controllers/ontology_controller.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/app/controllers/ontology_controller.py b/api/app/controllers/ontology_controller.py index 42d4bee0..c892b013 100644 --- a/api/app/controllers/ontology_controller.py +++ b/api/app/controllers/ontology_controller.py @@ -523,10 +523,9 @@ async def delete_scene( f"尝试删除系统默认场景: user_id={current_user.id}, " f"scene_id={scene_id}, scene_name={scene.scene_name}" ) - return fail( - BizCode.BAD_REQUEST, - "系统默认场景不可删除", - "该场景为系统预设场景,不允许删除" + raise HTTPException( + status_code=400, + detail="SYSTEM_DEFAULT_SCENE_CANNOT_DELETE" ) # 创建OntologyService实例 @@ -552,6 +551,9 @@ async def delete_scene( return success(data={"deleted": success_flag}, msg="场景删除成功") + except HTTPException: + raise + except ValueError as e: api_logger.warning(f"Validation error in scene deletion: {str(e)}") return fail(BizCode.BAD_REQUEST, "请求参数无效", str(e)) From 16c1cbe24fa9fe5c1f21d2c2c6a53e5fb292d5d0 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Thu, 5 Mar 2026 11:17:56 +0800 Subject: [PATCH 52/83] feat(agent): add input variable validation --- api/app/controllers/app_controller.py | 16 +- api/app/core/models/base.py | 59 +-- api/app/core/workflow/nodes/agent/node.py | 6 +- api/app/schemas/api_key_schema.py | 9 +- api/app/schemas/multi_agent_schema.py | 6 +- api/app/services/agent_tools.py | 4 +- api/app/services/app_chat_service.py | 198 ++------- api/app/services/app_service.py | 413 ------------------ api/app/services/draft_run_service.py | 437 +++++++++---------- api/app/services/langchain_tool_server.py | 37 +- api/app/services/multi_agent_orchestrator.py | 17 +- api/app/services/skill_service.py | 2 +- api/app/services/tool_service.py | 8 +- 13 files changed, 330 insertions(+), 882 deletions(-) diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 653f616c..cdf94345 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -396,10 +396,10 @@ async def draft_run( from app.models import AgentConfig, ModelConfig from sqlalchemy import select from app.core.exceptions import BusinessException - from app.services.draft_run_service import DraftRunService + from app.services.draft_run_service import AgentRunService service = AppService(db) - draft_service = DraftRunService(db) + draft_service = AgentRunService(db) # 1. 验证应用 app = service._get_app_or_404(app_id) @@ -484,8 +484,8 @@ async def draft_run( } ) - from app.services.draft_run_service import DraftRunService - draft_service = DraftRunService(db) + from app.services.draft_run_service import AgentRunService + draft_service = AgentRunService(db) result = await draft_service.run( agent_config=agent_cfg, model_config=model_config, @@ -789,8 +789,8 @@ async def draft_run_compare( # 流式返回 if payload.stream: async def event_generator(): - from app.services.draft_run_service import DraftRunService - draft_service = DraftRunService(db) + from app.services.draft_run_service import AgentRunService + draft_service = AgentRunService(db) async for event in draft_service.run_compare_stream( agent_config=agent_cfg, models=model_configs, @@ -820,8 +820,8 @@ async def draft_run_compare( ) # 非流式返回 - from app.services.draft_run_service import DraftRunService - draft_service = DraftRunService(db) + from app.services.draft_run_service import AgentRunService + draft_service = AgentRunService(db) result = await draft_service.run_compare( agent_config=agent_cfg, models=model_configs, diff --git a/api/app/core/models/base.py b/api/app/core/models/base.py index 5d4dbd10..dba6717d 100644 --- a/api/app/core/models/base.py +++ b/api/app/core/models/base.py @@ -21,6 +21,7 @@ from pydantic import BaseModel, Field T = TypeVar("T") + class RedBearModelConfig(BaseModel): """模型配置基类""" model_name: str @@ -32,17 +33,18 @@ class RedBearModelConfig(BaseModel): timeout: float = Field(default_factory=lambda: float(os.getenv("LLM_TIMEOUT", "120.0"))) # 最大重试次数 - 默认2次以避免过长等待,可通过环境变量 LLM_MAX_RETRIES 配置 max_retries: int = Field(default_factory=lambda: int(os.getenv("LLM_MAX_RETRIES", "2"))) - concurrency: int = 5 # 并发限流 + concurrency: int = 5 # 并发限流 extra_params: Dict[str, Any] = {} + class RedBearModelFactory: """模型工厂类""" - + @classmethod def get_model_params(cls, config: RedBearModelConfig) -> Dict[str, Any]: """根据提供商获取模型参数""" provider = config.provider.lower() - + # 打印供应商信息用于调试 from app.core.logging_config import get_business_logger logger = get_business_logger() @@ -87,7 +89,7 @@ class RedBearModelFactory: "timeout": timeout_config, "max_retries": config.max_retries, **config.extra_params - } + } elif provider == ModelProvider.DASHSCOPE: # DashScope (通义千问) 使用自己的参数格式 # 注意: DashScopeEmbeddings 不支持 timeout 和 base_url 参数 @@ -104,7 +106,7 @@ class RedBearModelFactory: # region 从 base_url 或 extra_params 获取 from botocore.config import Config as BotoConfig from app.core.models.bedrock_model_mapper import normalize_bedrock_model_id - + max_pool_connections = int(os.getenv("BEDROCK_MAX_POOL_CONNECTIONS", "50")) max_retries = int(os.getenv("BEDROCK_MAX_RETRIES", "2")) # Configure with increased connection pool @@ -112,16 +114,16 @@ class RedBearModelFactory: max_pool_connections=max_pool_connections, retries={'max_attempts': max_retries, 'mode': 'adaptive'} ) - + # 标准化模型 ID(自动转换简化名称为完整 Bedrock Model ID) model_id = normalize_bedrock_model_id(config.model_name) - + params = { "model_id": model_id, "config": boto_config, **config.extra_params } - + # 解析 API key (格式: access_key_id:secret_access_key) if config.api_key and ":" in config.api_key: access_key_id, secret_access_key = config.api_key.split(":", 1) @@ -129,51 +131,52 @@ class RedBearModelFactory: params["aws_secret_access_key"] = secret_access_key elif config.api_key: params["aws_access_key_id"] = config.api_key - + # 设置 region if config.base_url: params["region_name"] = config.base_url elif "region_name" not in params: params["region_name"] = "us-east-1" # 默认区域 - + return params else: raise BusinessException(f"不支持的提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) - + @classmethod def get_rerank_model_params(cls, config: RedBearModelConfig) -> Dict[str, Any]: """根据提供商获取模型参数""" provider = config.provider.lower() if provider in [ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]: - return { + return { "model": config.model_name, # "base_url": config.base_url, "jina_api_key": config.api_key, **config.extra_params - } + } else: raise BusinessException(f"不支持的提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) -def get_provider_llm_class(config:RedBearModelConfig, type: ModelType=ModelType.LLM) -> type[BaseLLM]: + +def get_provider_llm_class(config: RedBearModelConfig, type: ModelType = ModelType.LLM) -> type[BaseLLM]: """根据模型提供商获取对应的模型类""" provider = config.provider.lower() - + # dashscope 的 omni 模型使用 OpenAI 兼容模式 if provider == ModelProvider.DASHSCOPE and config.is_omni: from langchain_openai import ChatOpenAI return ChatOpenAI - - if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK] : + + if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK] : if type == ModelType.LLM: from langchain_openai import OpenAI - return OpenAI + return OpenAI elif type == ModelType.CHAT: from langchain_openai import ChatOpenAI return ChatOpenAI elif provider == ModelProvider.DASHSCOPE: from langchain_community.chat_models import ChatTongyi return ChatTongyi - elif provider == ModelProvider.OLLAMA: + elif provider == ModelProvider.OLLAMA: from langchain_ollama import OllamaLLM return OllamaLLM elif provider == ModelProvider.BEDROCK: @@ -183,15 +186,16 @@ def get_provider_llm_class(config:RedBearModelConfig, type: ModelType=ModelType. else: raise BusinessException(f"不支持的模型提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) + def get_provider_embedding_class(provider: str) -> type[Embeddings]: """根据模型提供商获取对应的模型类""" provider = provider.lower() - if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK] : + if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]: from langchain_openai import OpenAIEmbeddings - return OpenAIEmbeddings + return OpenAIEmbeddings elif provider == ModelProvider.DASHSCOPE: from langchain_community.embeddings import DashScopeEmbeddings - return DashScopeEmbeddings + return DashScopeEmbeddings elif provider == ModelProvider.OLLAMA: from langchain_ollama import OllamaEmbeddings return OllamaEmbeddings @@ -201,14 +205,15 @@ def get_provider_embedding_class(provider: str) -> type[Embeddings]: else: raise BusinessException(f"不支持的模型提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) + def get_provider_rerank_class(provider: str): """根据模型提供商获取对应的模型类""" - provider = provider.lower() - if provider in [ModelProvider.XINFERENCE, ModelProvider.GPUSTACK] : + provider = provider.lower() + if provider in [ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]: from langchain_community.document_compressors import JinaRerank - return JinaRerank - # elif provider == ModelProvider.OLLAMA: + return JinaRerank + # elif provider == ModelProvider.OLLAMA: # from langchain_ollama import OllamaEmbeddings # return OllamaEmbeddings else: - raise BusinessException(f"不支持的模型提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) \ No newline at end of file + raise BusinessException(f"不支持的模型提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) diff --git a/api/app/core/workflow/nodes/agent/node.py b/api/app/core/workflow/nodes/agent/node.py index 98d8bb75..3fbbbdbc 100644 --- a/api/app/core/workflow/nodes/agent/node.py +++ b/api/app/core/workflow/nodes/agent/node.py @@ -16,7 +16,7 @@ from app.core.workflow.nodes.base_node import BaseNode from app.core.workflow.variable.base_variable import VariableType from app.db import get_db from app.models import AppRelease -from app.services.draft_run_service import DraftRunService +from app.services.draft_run_service import AgentRunService logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class AgentNode(BaseNode): def _output_types(self) -> dict[str, VariableType]: return {"output": VariableType.STRING} - def _prepare_agent(self, variable_pool: VariablePool) -> tuple[DraftRunService, AppRelease, str]: + def _prepare_agent(self, variable_pool: VariablePool) -> tuple[AgentRunService, AppRelease, str]: """准备 Agent(公共逻辑) Args: @@ -65,7 +65,7 @@ class AgentNode(BaseNode): if not release: raise ValueError(f"Agent 不存在: {agent_id}") - draft_service = DraftRunService(db) + draft_service = AgentRunService(db) return draft_service, release, message diff --git a/api/app/schemas/api_key_schema.py b/api/app/schemas/api_key_schema.py index d19cf061..323c1a69 100644 --- a/api/app/schemas/api_key_schema.py +++ b/api/app/schemas/api_key_schema.py @@ -155,8 +155,7 @@ class ApiKey(BaseModel): return datetime.datetime.now() > self.expires_at @field_serializer('expires_at', 'last_used_at', 'created_at', 'updated_at') - @classmethod - def serialize_datetime(cls, v: Optional[datetime.datetime]) -> Optional[int]: + def serialize_datetime(self, v: Optional[datetime.datetime]) -> Optional[int]: """将datetime转换为时间戳""" return datetime_to_timestamp(v) @@ -171,8 +170,7 @@ class ApiKeyStats(BaseModel): avg_response_time: Optional[float] = Field(None, description="平均响应时间(毫秒)") @field_serializer('last_used_at') - @classmethod - def serialize_datetime(cls, v: Optional[datetime.datetime]) -> Optional[int]: + def serialize_datetime(self, v: Optional[datetime.datetime]) -> Optional[int]: """将datetime转换为时间戳""" return datetime_to_timestamp(v) @@ -219,7 +217,6 @@ class ApiKeyLog(BaseModel): created_at: datetime.datetime @field_serializer('created_at') - @classmethod - def serialize_datetime(cls, v: datetime.datetime) -> int: + def serialize_datetime(self, v: datetime.datetime) -> int: """将datetime转换为时间戳""" return datetime_to_timestamp(v) diff --git a/api/app/schemas/multi_agent_schema.py b/api/app/schemas/multi_agent_schema.py index 8fba2929..3573e87c 100644 --- a/api/app/schemas/multi_agent_schema.py +++ b/api/app/schemas/multi_agent_schema.py @@ -64,14 +64,14 @@ class ExecutionConfig(BaseModel): class MultiAgentConfigCreate(BaseModel): """创建多 Agent 配置""" master_agent_id: uuid.UUID = Field(..., description="主 Agent ID") - master_agent_name: Optional[str] = Field(None, max_length=100, description="主 Agent 名称") + master_agent_name: Optional[str] = Field(default=None, max_length=100, description="主 Agent 名称") orchestration_mode: str = Field( default="collaboration", pattern="^(collaboration|supervisor)$", description="协作模式:collaboration(协作)| supervisor(监督)" ) sub_agents: List[SubAgentConfig] = Field(..., description="子 Agent 列表") - routing_rules: Optional[List[RoutingRule]] = Field(None, description="路由规则") + routing_rules: Optional[List[RoutingRule]] = Field(default=None, description="路由规则") execution_config: ExecutionConfig = Field(default_factory=ExecutionConfig, description="执行配置") aggregation_strategy: str = Field( default="merge", @@ -83,7 +83,7 @@ class MultiAgentConfigCreate(BaseModel): class MultiAgentConfigUpdate(BaseModel): """更新多 Agent 配置""" master_agent_id: Optional[uuid.UUID] = None - master_agent_name: Optional[str] = Field(None, max_length=100, description="主 Agent 名称") + master_agent_name: Optional[str] = Field(default=None, max_length=100, description="主 Agent 名称") default_model_config_id: Optional[uuid.UUID] = Field(None, description="默认模型配置ID") model_parameters: Optional[ModelParameters] = Field( None, diff --git a/api/app/services/agent_tools.py b/api/app/services/agent_tools.py index 3ca7bddd..a4768b51 100644 --- a/api/app/services/agent_tools.py +++ b/api/app/services/agent_tools.py @@ -263,8 +263,8 @@ def create_agent_invocation_tool( try: # 9. 调用 Agent - from app.services.draft_run_service import DraftRunService - draft_service = DraftRunService(db) + from app.services.draft_run_service import AgentRunService + draft_service = AgentRunService(db) result = await draft_service.run( agent_config=agent_config, diff --git a/api/app/services/app_chat_service.py b/api/app/services/app_chat_service.py index e6ac227b..5430d2f9 100644 --- a/api/app/services/app_chat_service.py +++ b/api/app/services/app_chat_service.py @@ -10,25 +10,24 @@ from sqlalchemy.orm import Session from app.core.agent.agent_middleware import AgentMiddleware from app.core.agent.langchain_agent import LangChainAgent -from app.core.error_codes import BizCode from app.core.exceptions import BusinessException from app.core.logging_config import get_business_logger -from app.db import get_db, get_db_context -from app.models import MultiAgentConfig, AgentConfig, WorkflowConfig -from app.schemas import DraftRunRequest -from app.schemas.app_schema import FileInput -from app.services.tool_service import ToolService -from app.repositories.tool_repository import ToolRepository from app.db import get_db from app.models import MultiAgentConfig, AgentConfig +from app.models import WorkflowConfig +from app.repositories.tool_repository import ToolRepository +from app.schemas import DraftRunRequest +from app.schemas.app_schema import FileInput from app.schemas.prompt_schema import render_prompt_message, PromptMessageRole from app.services.conversation_service import ConversationService -from app.services.draft_run_service import create_knowledge_retrieval_tool, create_long_term_memory_tool +from app.services.draft_run_service import create_knowledge_retrieval_tool, create_long_term_memory_tool, \ + AgentRunService from app.services.draft_run_service import create_web_search_tool from app.services.model_service import ModelApiKeyService from app.services.multi_agent_orchestrator import MultiAgentOrchestrator -from app.services.workflow_service import WorkflowService from app.services.multimodal_service import MultimodalService +from app.services.tool_service import ToolService +from app.services.workflow_service import WorkflowService logger = get_business_logger() @@ -39,6 +38,8 @@ class AppChatService: def __init__(self, db: Session): self.db = db self.conversation_service = ConversationService(db) + self.agent_service = AgentRunService(db) + self.workflow_service = WorkflowService(db) async def agnet_chat( self, @@ -55,12 +56,10 @@ class AppChatService: files: Optional[List[FileInput]] = None # 新增:多模态文件 ) -> Dict[str, Any]: """聊天(非流式)""" - start_time = time.time() config_id = None - if variables is None: - variables = {} + variables = self.agent_service.prepare_variables(variables, config.variables) # 获取模型配置ID model_config_id = config.default_model_config_id @@ -79,74 +78,20 @@ class AppChatService: tools = [] # 获取工具服务 - tool_service = ToolService(self.db) tenant_id = ToolRepository.get_tenant_id_by_workspace_id(self.db, str(workspace_id)) - # 从配置中获取启用的工具 - if hasattr(config, 'tools') and config.tools and isinstance(config.tools, list): - for tool_config in config.tools: - if tool_config.get("enabled", False): - # 根据工具名称查找工具实例 - tool_instance = tool_service._get_tool_instance(tool_config.get("tool_id", ""), tenant_id) - if tool_instance: - if tool_instance.name == "baidu_search_tool" and not web_search: - continue - # 转换为LangChain工具 - langchain_tool = tool_instance.to_langchain_tool(tool_config.get("operation", None)) - tools.append(langchain_tool) - elif hasattr(config, 'tools') and config.tools and isinstance(config.tools, dict): - web_tools = config.tools - web_search_choice = web_tools.get("web_search", {}) - web_search_enable = web_search_choice.get("enabled", False) - if web_search: - if web_search_enable: - search_tool = create_web_search_tool({}) - tools.append(search_tool) - - logger.debug( - "已添加网络搜索工具", - extra={ - "tool_count": len(tools) - } - ) - - # 加载技能关联的工具 - if hasattr(config, 'skills') and config.skills: - skills = config.skills - skill_enable = skills.get("enabled", False) - if skill_enable: - middleware = AgentMiddleware(skills=skills) - skill_tools, skill_configs, tool_to_skill_map = middleware.load_skill_tools(self.db, tenant_id) - tools.extend(skill_tools) - logger.debug(f"已加载 {len(skill_tools)} 个技能工具") - - # 应用动态过滤 - if skill_configs: - tools, activated_skill_ids = middleware.filter_tools(tools, message, skill_configs, - tool_to_skill_map) - logger.debug(f"过滤后剩余 {len(tools)} 个工具") - active_prompts = AgentMiddleware.get_active_prompts( - activated_skill_ids, skill_configs - ) - system_prompt = f"{system_prompt}\n\n{active_prompts}" - - # 添加知识库检索工具 - knowledge_retrieval = config.knowledge_retrieval - if knowledge_retrieval: - knowledge_bases = knowledge_retrieval.get("knowledge_bases", []) - kb_ids = [kb.get("kb_id") for kb in knowledge_bases if kb.get("kb_id")] - if kb_ids: - kb_tool = create_knowledge_retrieval_tool(knowledge_retrieval, kb_ids, user_id) - tools.append(kb_tool) - - # 添加长期记忆工具 + tools.extend(self.agent_service.load_tools_config(config.tools, web_search, tenant_id)) + skill_tools, skill_prompts = self.agent_service.load_skill_config(config.skills, message, tenant_id) + tools.extend(skill_tools) + if skill_prompts: + system_prompt = f"{system_prompt}\n\n{skill_prompts}" + tools.extend(self.agent_service.load_knowledge_retrieval_config(config.knowledge_retrieval, user_id)) memory_flag = False - if memory == True: - memory_config = config.memory - if memory_config.get("enabled") and user_id: - memory_flag = True - memory_tool = create_long_term_memory_tool(memory_config, user_id) - tools.append(memory_tool) + if memory: + memory_tools, memory_flag = self.agent_service.load_memory_config( + config.memory, user_id, storage_type, user_rag_memory_id + ) + tools.extend(memory_tools) # 获取模型参数 model_parameters = config.model_parameters @@ -246,10 +191,9 @@ class AppChatService: try: start_time = time.time() config_id = None + yield f"event: start\ndata: {json.dumps({'conversation_id': str(conversation_id)}, ensure_ascii=False)}\n\n" - if variables is None: - variables = {} - + variables = self.agent_service.prepare_variables(variables, config.variables) # 获取模型配置ID model_config_id = config.default_model_config_id api_key_obj = ModelApiKeyService.get_available_api_key(self.db, model_config_id) @@ -267,73 +211,22 @@ class AppChatService: tools = [] # 获取工具服务 - tool_service = ToolService(self.db) tenant_id = ToolRepository.get_tenant_id_by_workspace_id(self.db, str(workspace_id)) - if hasattr(config, 'tools') and config.tools and isinstance(config.tools, list): - for tool_config in config.tools: - if tool_config.get("enabled", False): - # 根据工具名称查找工具实例 - tool_instance = tool_service._get_tool_instance(tool_config.get("tool_id", ""), tenant_id) - if tool_instance: - if tool_instance.name == "baidu_search_tool" and not web_search: - continue - # 转换为LangChain工具 - langchain_tool = tool_instance.to_langchain_tool(tool_config.get("operation", None)) - tools.append(langchain_tool) - elif hasattr(config, 'tools') and config.tools and isinstance(config.tools, dict): - web_tools = config.tools - web_search_choice = web_tools.get("web_search", {}) - web_search_enable = web_search_choice.get("enabled", False) - if web_search: - if web_search_enable: - search_tool = create_web_search_tool({}) - tools.append(search_tool) - - logger.debug( - "已添加网络搜索工具", - extra={ - "tool_count": len(tools) - } - ) - - # 加载技能关联的工具 - if hasattr(config, 'skills') and config.skills: - skills = config.skills - skill_enable = skills.get("enabled", False) - if skill_enable: - middleware = AgentMiddleware(skills=skills) - skill_tools, skill_configs, tool_to_skill_map = middleware.load_skill_tools(self.db, tenant_id) - tools.extend(skill_tools) - logger.debug(f"已加载 {len(skill_tools)} 个技能工具") - - # 应用动态过滤 - if skill_configs: - tools, activated_skill_ids = middleware.filter_tools(tools, message, skill_configs, - tool_to_skill_map) - logger.debug(f"过滤后剩余 {len(tools)} 个工具") - active_prompts = AgentMiddleware.get_active_prompts( - activated_skill_ids, skill_configs - ) - system_prompt = f"{system_prompt}\n\n{active_prompts}" - - # 添加知识库检索工具 - knowledge_retrieval = config.knowledge_retrieval - if knowledge_retrieval: - knowledge_bases = knowledge_retrieval.get("knowledge_bases", []) - kb_ids = [kb.get("kb_id") for kb in knowledge_bases if kb.get("kb_id")] - if kb_ids: - kb_tool = create_knowledge_retrieval_tool(knowledge_retrieval, kb_ids, user_id) - tools.append(kb_tool) + tools.extend(self.agent_service.load_tools_config(config.tools, web_search, tenant_id)) + skill_tools, skill_prompts = self.agent_service.load_skill_config(config.skills, message, tenant_id) + tools.extend(skill_tools) + if skill_prompts: + system_prompt = f"{system_prompt}\n\n{skill_prompts}" + tools.extend(self.agent_service.load_knowledge_retrieval_config(config.knowledge_retrieval, user_id)) # 添加长期记忆工具 memory_flag = False if memory: - memory_config = config.memory - if memory_config.get("enabled") and user_id: - memory_flag = True - memory_tool = create_long_term_memory_tool(memory_config, user_id) - tools.append(memory_tool) + memory_tools, memory_flag = self.agent_service.load_memory_config( + config.memory, user_id, storage_type, user_rag_memory_id + ) + tools.extend(memory_tools) # 获取模型参数 model_parameters = config.model_parameters @@ -372,9 +265,6 @@ class AppChatService: processed_files = await multimodal_service.process_files(files) logger.info(f"处理了 {len(processed_files)} 个文件") - # 发送开始事件 - yield f"event: start\ndata: {json.dumps({'conversation_id': str(conversation_id)}, ensure_ascii=False)}\n\n" - # 流式调用 Agent(支持多模态) full_content = "" total_tokens = 0 @@ -418,7 +308,7 @@ class AppChatService: ModelApiKeyService.record_api_key_usage(self.db, api_key_obj.id) # 发送结束事件 - end_data = {"elapsed_time": elapsed_time, "message_length": len(full_content)} + end_data = {"elapsed_time": elapsed_time, "message_length": len(full_content), "error": None} yield f"event: end\ndata: {json.dumps(end_data, ensure_ascii=False)}\n\n" logger.info( @@ -437,7 +327,7 @@ class AppChatService: except Exception as e: logger.error(f"流式聊天失败: {str(e)}", exc_info=True) # 发送错误事件 - yield f"event: error\ndata: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n" + yield f"event: end\ndata: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n" async def multi_agent_chat( self, @@ -491,10 +381,10 @@ class AppChatService: "mode": result.get("mode"), "elapsed_time": result.get("elapsed_time"), "usage": result.get("usage", { - "prompt_tokens": 0, - "completion_tokens": 0, - "total_tokens": 0 - }) + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + }) } ) @@ -524,8 +414,6 @@ class AppChatService: """多 Agent 聊天(流式)""" start_time = time.time() - actual_config_id = None - config_id = actual_config_id if variables is None: variables = {} @@ -631,7 +519,6 @@ class AppChatService: user_rag_memory_id: Optional[str] = None, ) -> Dict[str, Any]: """聊天(非流式)""" - workflow_service = WorkflowService(self.db) payload = DraftRunRequest( message=message, variables=variables, @@ -639,7 +526,7 @@ class AppChatService: stream=True, user_id=user_id ) - return await workflow_service.run( + return await self.workflow_service.run( app_id=app_id, payload=payload, config=config, @@ -666,7 +553,6 @@ class AppChatService: ) -> AsyncGenerator[dict, None]: """聊天(流式)""" - workflow_service = WorkflowService(self.db) payload = DraftRunRequest( message=message, variables=variables, @@ -675,7 +561,7 @@ class AppChatService: user_id=user_id, files=files ) - async for event in workflow_service.run_stream( + async for event in self.workflow_service.run_stream( app_id=app_id, payload=payload, config=config, diff --git a/api/app/services/app_service.py b/api/app/services/app_service.py index c5919af9..a248f869 100644 --- a/api/app/services/app_service.py +++ b/api/app/services/app_service.py @@ -1791,372 +1791,6 @@ class AppService: return shares - # ==================== 试运行功能 ==================== - - async def draft_run( - self, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None - ) -> Dict[str, Any]: - """试运行 Agent(使用当前草稿配置) - - Args: - app_id: 应用ID - message: 用户消息 - conversation_id: 会话ID(用于多轮对话) - user_id: 用户ID(用于会话管理) - variables: 自定义变量参数值 - workspace_id: 工作空间ID(用于权限验证) - - Returns: - Dict: 包含 AI 回复和元数据的字典 - - Raises: - ResourceNotFoundException: 当应用不存在时 - BusinessException: 当应用类型不支持或配置缺失时 - """ - from app.services.draft_run_service import DraftRunService - - logger.info("试运行 Agent", extra={"app_id": str(app_id), "user_message": message[:50]}) - - # 1. 验证应用 - app = self._get_app_or_404(app_id) - - if app.type != "agent": - raise BusinessException("只有 Agent 类型应用支持试运行", BizCode.APP_TYPE_NOT_SUPPORTED) - - # 只读操作,允许访问共享应用 - self._validate_app_accessible(app, workspace_id) - - # 2. 获取 Agent 配置 - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id) - agent_cfg = self.db.scalars(stmt).first() - - if not agent_cfg: - raise BusinessException("Agent 配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING) - - # 3. 获取模型配置 - model_config = None - if agent_cfg.default_model_config_id: - from app.models import ModelConfig - model_config = self.db.get(ModelConfig, agent_cfg.default_model_config_id) - - if not model_config: - raise BusinessException("模型配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING) - - # 4. 调用试运行服务 - logger.debug( - "准备调用试运行服务", - extra={ - "app_id": str(app_id), - "model": model_config.name, - "has_conversation_id": bool(conversation_id), - "has_variables": bool(variables) - } - ) - - draft_service = DraftRunService(self.db) - result = await draft_service.run( - agent_config=agent_cfg, - model_config=model_config, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables - ) - - logger.debug( - "试运行服务返回结果", - extra={ - "result_type": str(type(result)), - "result_keys": list(result.keys()) if isinstance(result, dict) else "not_dict", - "has_message": "message" in result if isinstance(result, dict) else False, - "has_conversation_id": "conversation_id" in result if isinstance(result, dict) else False - } - ) - - logger.info( - "试运行完成", - extra={ - "app_id": str(app_id), - "elapsed_time": result.get("elapsed_time"), - "model": model_config.name - } - ) - - return result - - async def draft_run_stream( - self, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None - ): - """试运行 Agent(流式返回) - - Args: - app_id: 应用ID - message: 用户消息 - conversation_id: 会话ID(用于多轮对话) - user_id: 用户ID(用于会话管理) - variables: 自定义变量参数值 - workspace_id: 工作空间ID(用于权限验证) - - Yields: - str: SSE 格式的事件数据 - - Raises: - ResourceNotFoundException: 当应用不存在时 - BusinessException: 当应用类型不支持或配置缺失时 - """ - from app.services.draft_run_service import DraftRunService - - logger.info("流式试运行 Agent", extra={"app_id": str(app_id), "user_message": message[:50]}) - - # 1. 验证应用 - app = self._get_app_or_404(app_id) - - if app.type != "agent": - raise BusinessException("只有 Agent 类型应用支持试运行", BizCode.APP_TYPE_NOT_SUPPORTED) - - # 只读操作,允许访问共享应用 - self._validate_app_accessible(app, workspace_id) - - # 2. 获取 Agent 配置 - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id) - agent_cfg = self.db.scalars(stmt).first() - - if not agent_cfg: - raise BusinessException("Agent 配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING) - - # 3. 获取模型配置 - model_config = None - if agent_cfg.default_model_config_id: - from app.models import ModelConfig - model_config = self.db.get(ModelConfig, agent_cfg.default_model_config_id) - - if not model_config: - raise BusinessException("模型配置不存在,无法试运行", BizCode.AGENT_CONFIG_MISSING) - - # 4. 调用流式试运行服务 - draft_service = DraftRunService(self.db) - async for event in draft_service.run_stream( - agent_config=agent_cfg, - model_config=model_config, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables - ): - yield event - - # ==================== 多模型对比试运行 ==================== - - async def draft_run_compare( - self, - *, - app_id: uuid.UUID, - message: str, - models: List[app_schema.ModelCompareItem], - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None, - parallel: bool = True, - timeout: int = 60 - ) -> Dict[str, Any]: - """多模型对比试运行 - - Args: - app_id: 应用ID - message: 用户消息 - models: 要对比的模型列表 - conversation_id: 会话ID - user_id: 用户ID - variables: 变量参数 - workspace_id: 工作空间ID - parallel: 是否并行执行 - timeout: 超时时间(秒) - - Returns: - Dict: 对比结果 - """ - from app.models import ModelConfig - from app.services.draft_run_service import DraftRunService - - logger.info( - "多模型对比试运行", - extra={ - "app_id": str(app_id), - "model_count": len(models), - "parallel": parallel - } - ) - - # 1. 验证应用 - app = self._get_app_or_404(app_id) - if app.type != "agent": - raise BusinessException("只有 Agent 类型应用支持试运行", BizCode.APP_TYPE_NOT_SUPPORTED) - - # 只读操作,允许访问共享应用 - self._validate_app_accessible(app, workspace_id) - - # 2. 获取 Agent 配置 - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id) - agent_cfg = self.db.scalars(stmt).first() - if not agent_cfg: - raise BusinessException("Agent 配置不存在", BizCode.AGENT_CONFIG_MISSING) - - # 3. 准备所有模型配置 - model_configs = [] - for model_item in models: - model_config = self.db.get(ModelConfig, model_item.model_config_id) - if not model_config: - raise ResourceNotFoundException("模型配置", str(model_item.model_config_id)) - - # 合并参数:agent配置参数 + 请求覆盖参数 - merged_parameters = { - **(agent_cfg.model_parameters or {}), - **(model_item.model_parameters or {}) - } - - model_configs.append({ - "model_config": model_config, - "parameters": merged_parameters, - "label": model_item.label or model_config.name, - "model_config_id": model_item.model_config_id - }) - - # 4. 调用 DraftRunService 的对比方法 - draft_service = DraftRunService(self.db) - result = await draft_service.run_compare( - agent_config=agent_cfg, - models=model_configs, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - parallel=parallel, - timeout=timeout - ) - - logger.info( - "多模型对比完成", - extra={ - "app_id": str(app_id), - "successful": result["successful_count"], - "failed": result["failed_count"] - } - ) - - return result - - async def draft_run_compare_stream( - self, - *, - app_id: uuid.UUID, - message: str, - models: List[app_schema.ModelCompareItem], - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None, - parallel: bool = True, - timeout: int = 60 - ): - """多模型对比试运行(流式返回) - - Args: - app_id: 应用ID - message: 用户消息 - models: 要对比的模型列表 - conversation_id: 会话ID - user_id: 用户ID - variables: 变量参数 - workspace_id: 工作空间ID - timeout: 超时时间(秒) - - Yields: - str: SSE 格式的事件数据 - """ - from app.models import ModelConfig - from app.services.draft_run_service import DraftRunService - - logger.info( - "多模型对比流式试运行", - extra={ - "app_id": str(app_id), - "model_count": len(models) - } - ) - - # 1. 验证应用 - app = self._get_app_or_404(app_id) - if app.type != "agent": - raise BusinessException("只有 Agent 类型应用支持试运行", BizCode.APP_TYPE_NOT_SUPPORTED) - - # 只读操作,允许访问共享应用 - self._validate_app_accessible(app, workspace_id) - - # 2. 获取 Agent 配置 - stmt = select(AgentConfig).where(AgentConfig.app_id == app_id) - agent_cfg = self.db.scalars(stmt).first() - if not agent_cfg: - raise BusinessException("Agent 配置不存在", BizCode.AGENT_CONFIG_MISSING) - - # 3. 准备所有模型配置 - model_configs = [] - for model_item in models: - model_config = self.db.get(ModelConfig, model_item.model_config_id) - if not model_config: - raise ResourceNotFoundException("模型配置", str(model_item.model_config_id)) - - # 合并参数:agent配置参数 + 请求覆盖参数 - merged_parameters = { - **(agent_cfg.model_parameters or {}), - **(model_item.model_parameters or {}) - } - - model_configs.append({ - "model_config": model_config, - "parameters": merged_parameters, - "label": model_item.label or model_config.name, - "model_config_id": model_item.model_config_id - }) - - # 4. 调用 DraftRunService 的流式对比方法 - draft_service = DraftRunService(self.db) - async for event in draft_service.run_compare_stream( - agent_config=agent_cfg, - models=model_configs, - message=message, - workspace_id=workspace_id, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - parallel=parallel, - timeout=timeout - ): - yield event - - logger.info( - "多模型对比流式完成", - extra={"app_id": str(app_id)} - ) - - # ==================== 向后兼容的函数接口 ==================== # 保留函数接口以兼容现有代码,但内部使用服务类 @@ -2278,53 +1912,6 @@ def get_apps_by_ids( return service.get_apps_by_ids(app_ids, workspace_id) -# ==================== 向后兼容的函数接口 ==================== - -async def draft_run( - db: Session, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None -) -> Dict[str, Any]: - """试运行 Agent(向后兼容接口)""" - service = AppService(db) - return await service.draft_run( - app_id=app_id, - message=message, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - workspace_id=workspace_id - ) - - -async def draft_run_stream( - db: Session, - *, - app_id: uuid.UUID, - message: str, - conversation_id: Optional[str] = None, - user_id: Optional[str] = None, - variables: Optional[Dict[str, Any]] = None, - workspace_id: Optional[uuid.UUID] = None -): - """试运行 Agent 流式返回(向后兼容接口)""" - service = AppService(db) - async for event in service.draft_run_stream( - app_id=app_id, - message=message, - conversation_id=conversation_id, - user_id=user_id, - variables=variables, - workspace_id=workspace_id - ): - yield event - - # ==================== 依赖注入函数 ==================== def get_app_service( diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 693f1a26..0cf68be2 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -17,6 +17,7 @@ from sqlalchemy.orm import Session from app.celery_app import celery_app from app.core.agent.agent_middleware import AgentMiddleware +from app.core.agent.langchain_agent import LangChainAgent from app.core.error_codes import BizCode from app.core.exceptions import BusinessException from app.core.logging_config import get_business_logger @@ -26,6 +27,7 @@ from app.repositories.tool_repository import ToolRepository from app.schemas.app_schema import FileInput from app.schemas.prompt_schema import PromptMessageRole, render_prompt_message from app.services import task_service +from app.services.conversation_service import ConversationService from app.services.langchain_tool_server import Search from app.services.memory_agent_service import MemoryAgentService from app.services.model_parameter_merger import ModelParameterMerger @@ -52,8 +54,12 @@ class LongTermMemoryInput(BaseModel): description="经过优化重写的查询问题。请将用户的原始问题重写为更合适的检索形式,包含关键词,上下文和具体描述,注意错词检查并且改写") -def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str, storage_type: Optional[str] = None, - user_rag_memory_id: Optional[str] = None): +def create_long_term_memory_tool( + memory_config: Dict[str, Any], + end_user_id: str, + storage_type: Optional[str] = None, + user_rag_memory_id: Optional[str] = None +): """创建记忆工具, @@ -61,6 +67,7 @@ def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str memory_config: 记忆配置 end_user_id: 用户ID storage_type: 存储类型(可选) + user_rag_memory_id: 用户RAG记忆ID(可选) Returns: 长期记忆工具 @@ -188,7 +195,9 @@ def create_knowledge_retrieval_tool(kb_config, kb_ids, user_id): """从知识库中检索相关信息。当用户的问题需要参考知识库、文档或历史记录时,使用此工具进行检索。 Args: - query: 需要检索的问题或关键词 + kb_config: 知识库配置 + kb_ids: 知识库ID列表 + user_id: 用户ID Returns: 检索到的相关知识内容 @@ -232,17 +241,141 @@ def create_knowledge_retrieval_tool(kb_config, kb_ids, user_id): return knowledge_retrieval_tool -class DraftRunService: - """试运行服务类""" +class AgentRunService: + """Agent运行服务类""" def __init__(self, db: Session): - """初始化试运行服务 + """Agent运行服务 Args: db: 数据库会话 """ self.db = db + @staticmethod + def prepare_variables( + input_vars: dict | None, + variables_config: dict | None + ) -> dict: + input_vars = input_vars or {} + for variable in variables_config: + if variable.get("required") and variable.get("name") not in input_vars: + raise ValueError(f"The required parameter '{variable.get('name')}' was not provided") + return input_vars + + def load_tools_config(self, tools_config, web_search, tenant_id) -> list: + """加载工具配置""" + if not tools_config: + return [] + tools = [] + tool_service = ToolService(self.db) + + if tools_config and isinstance(tools_config, list): + for tool_config in tools_config: + if tool_config.get("enabled", False): + # 根据工具名称查找工具实例 + tool_instance = tool_service.get_tool_instance(tool_config.get("tool_id", ""), tenant_id) + if tool_instance: + if tool_instance.name == "baidu_search_tool" and not web_search: + continue + # 转换为LangChain工具 + langchain_tool = tool_instance.to_langchain_tool(tool_config.get("operation", None)) + tools.append(langchain_tool) + elif tools_config and isinstance(tools_config, dict): + web_search_choice = tools_config.get("web_search", {}) + web_search_enable = web_search_choice.get("enabled", False) + if web_search and web_search_enable: + search_tool = create_web_search_tool({}) + tools.append(search_tool) + + logger.debug( + "已添加网络搜索工具", + extra={ + "tool_count": len(tools) + } + ) + return tools + + def load_skill_config( + self, + skills_config: dict | None, + message: str, tenant_id + ) -> tuple[list, str]: + if not skills_config: + return [], "" + + tools = [] + skill_prompts = "" + skill_enable = skills_config.get("enabled", False) + if skill_enable: + middleware = AgentMiddleware(skills=skills_config) + skill_tools, skill_configs, tool_to_skill_map = middleware.load_skill_tools(self.db, tenant_id) + tools.extend(skill_tools) + logger.debug(f"已加载 {len(skill_tools)} 个技能工具") + + if skill_configs: + tools, activated_skill_ids = middleware.filter_tools(tools, message, skill_configs, + tool_to_skill_map) + logger.debug(f"过滤后剩余 {len(tools)} 个工具") + skill_prompts = AgentMiddleware.get_active_prompts( + activated_skill_ids, skill_configs + ) + + return tools, skill_prompts + + def load_knowledge_retrieval_config( + self, + knowledge_retrieval_config: dict | None, + user_id + ) -> list: + if not knowledge_retrieval_config: + return [] + + tools = [] + knowledge_bases = knowledge_retrieval_config.get("knowledge_bases", []) + kb_ids = bool(knowledge_bases and knowledge_bases[0].get("kb_id")) + if kb_ids: + # 创建知识库检索工具 + kb_tool = create_knowledge_retrieval_tool(knowledge_retrieval_config, kb_ids, user_id) + tools.append(kb_tool) + + logger.debug( + "已添加知识库检索工具", + extra={ + "kb_ids": kb_ids, + "tool_count": len(tools) + } + ) + return tools + + def load_memory_config( + self, + memory_config: dict | None, + user_id, + storage_type, + user_rag_memory_id + ) -> tuple[list, bool]: + """加载长期记忆配置""" + if not memory_config: + return [], False + + tools = [] + if memory_config.get("enabled"): + if user_id: + # 创建长期记忆工具 + memory_tool = create_long_term_memory_tool(memory_config, user_id, storage_type, + user_rag_memory_id) + tools.append(memory_tool) + + logger.debug( + "已添加长期记忆工具", + extra={ + "user_id": user_id, + "tool_count": len(tools) + } + ) + return tools, bool(memory_config.get("enabled")) + async def run( self, *, @@ -270,19 +403,21 @@ class DraftRunService: conversation_id: 会话ID(用于多轮对话) user_id: 用户ID variables: 自定义变量参数值 + storage_type: 存储类型(可选) + user_rag_memory_id: 用户RAG记忆ID(可选) + web_search: 是否启用网络搜索(默认True) + memory: 是否启用长期记忆(默认True) + sub_agent: 是否为子代理调用(默认False) + files: 多模态文件列表(可选) Returns: Dict: 包含 AI 回复和元数据的字典 """ - memory_flag = False - - print('===========', storage_type) - - print(user_id) - if variables == None: variables = {} - from app.core.agent.langchain_agent import LangChainAgent - start_time = time.time() + tools_config: dict | list | None = agent_config.tools + skills_config: dict | None = agent_config.skills + knowledge_retrieval_config: dict | None = agent_config.knowledge_retrieval + memory_config: dict | None = agent_config.memory try: # 1. 获取 API Key 配置 @@ -302,112 +437,40 @@ class DraftRunService: agent_config=agent_config ) - items_params = variables + if sub_agent: + variables = self.prepare_variables(variables, agent_config.variables) + else: + # FIXME: subagent input valid + variables = variables or {} + system_prompt = render_prompt_message( - agent_config.system_prompt, # 修正拼写错误 + agent_config.system_prompt, PromptMessageRole.USER, - items_params + variables ) # 3. 处理系统提示词(支持变量替换) system_prompt = system_prompt.get_text_content() or "你是一个专业的AI助手" - print('系统提示词:', system_prompt) # 4. 准备工具列表 tools = [] - tool_service = ToolService(self.db) tenant_id = ToolRepository.get_tenant_id_by_workspace_id(self.db, str(workspace_id)) # 从配置中获取启用的工具 - if hasattr(agent_config, 'tools') and agent_config.tools and isinstance(agent_config.tools, list): - if hasattr(agent_config, 'tools') and agent_config.tools: - for tool_config in agent_config.tools: - print("+" * 50) - print(f"agent_config:{agent_config}") - print(f"tool_config:{tool_config}") - if tool_config.get("enabled", False): - # 根据工具名称查找工具实例 - tool_instance = tool_service._get_tool_instance(tool_config.get("tool_id", ""), tenant_id) - if tool_instance: - if tool_instance.name == "baidu_search_tool" and not web_search: - continue - # 转换为LangChain工具 - langchain_tool = tool_instance.to_langchain_tool(tool_config.get("operation", None)) - tools.append(langchain_tool) - elif hasattr(agent_config, 'tools') and agent_config.tools and isinstance(agent_config.tools, dict): - web_tools = agent_config.tools - web_search_choice = web_tools.get("web_search", {}) - web_search_enable = web_search_choice.get("enabled", False) - if web_search: - if web_search_enable: - search_tool = create_web_search_tool({}) - tools.append(search_tool) - - logger.debug( - "已添加网络搜索工具", - extra={ - "tool_count": len(tools) - } - ) - - # 加载技能关联的工具 - if hasattr(agent_config, 'skills') and agent_config.skills: - skills = agent_config.skills - skill_enable = skills.get("enabled", False) - if skill_enable: - middleware = AgentMiddleware(skills=skills) - skill_tools, skill_configs, tool_to_skill_map = middleware.load_skill_tools(self.db, tenant_id) - tools.extend(skill_tools) - logger.debug(f"已加载 {len(skill_tools)} 个技能工具") - - # 应用动态过滤 - if skill_configs: - tools, activated_skill_ids = middleware.filter_tools(tools, message, skill_configs, - tool_to_skill_map) - logger.debug(f"过滤后剩余 {len(tools)} 个工具") - active_prompts = AgentMiddleware.get_active_prompts( - activated_skill_ids, skill_configs - ) - system_prompt = f"{system_prompt}\n\n{active_prompts}" - - # 添加知识库检索工具 - if agent_config.knowledge_retrieval: - kb_config = agent_config.knowledge_retrieval - knowledge_bases = kb_config.get("knowledge_bases", []) - kb_ids = bool(knowledge_bases and knowledge_bases[0].get("kb_id")) - if kb_ids: - # 创建知识库检索工具 - kb_tool = create_knowledge_retrieval_tool(kb_config, kb_ids, user_id) - tools.append(kb_tool) - - logger.debug( - "已添加知识库检索工具", - extra={ - "kb_ids": kb_ids, - "tool_count": len(tools) - } - ) - + tools.extend(self.load_tools_config(tools_config, web_search, tenant_id)) + skill_tools, skill_prompts = self.load_skill_config(skills_config, message, tenant_id) + tools.extend(skill_tools) + if skill_prompts: + system_prompt = f"{system_prompt}\n\n{skill_prompts}" + tools.extend(self.load_knowledge_retrieval_config(knowledge_retrieval_config, user_id)) # 添加长期记忆工具 + memory_flag = False if memory: - if agent_config.memory and agent_config.memory.get("enabled"): - memory_flag = True - - memory_config = agent_config.memory - if user_id: - # 创建长期记忆工具 - memory_tool = create_long_term_memory_tool(memory_config, user_id, storage_type, - user_rag_memory_id) - tools.append(memory_tool) - - logger.debug( - "已添加长期记忆工具", - extra={ - "user_id": user_id, - "tool_count": len(tools) - } - ) + memory_tools, memory_flag = self.load_memory_config( + memory_config, user_id, storage_type, user_rag_memory_id + ) + tools.extend(memory_tools) # 4. 创建 LangChain Agent agent = LangChainAgent( @@ -432,7 +495,7 @@ class DraftRunService: # 6. 加载历史消息 history = [] - if agent_config.memory and agent_config.memory.get("enabled"): + if memory_config and memory_config.get("enabled"): history = await self._load_conversation_history( conversation_id=conversation_id, max_history=agent_config.memory.get("max_history", 10) @@ -482,7 +545,7 @@ class DraftRunService: ModelApiKeyService.record_api_key_usage(self.db, api_key_config.get("api_key_id")) # 9. 保存会话消息 - if not sub_agent and agent_config.memory and agent_config.memory.get("enabled"): + if not sub_agent and memory_config and memory_config.get("enabled"): await self._save_conversation_message( conversation_id=conversation_id, user_message=message, @@ -557,16 +620,21 @@ class DraftRunService: Yields: str: SSE 格式的事件数据 """ - memory_flag = False - if variables == None: variables = {} - - from app.core.agent.langchain_agent import LangChainAgent + tools_config: dict | list | None = agent_config.tools + skills_config: dict | None = agent_config.skills + knowledge_retrieval_config: dict | None = agent_config.knowledge_retrieval + memory_config: dict | None = agent_config.memory start_time = time.time() try: # 1. 获取 API Key 配置 api_key_config = await self._get_api_key(model_config.id) + if not sub_agent: + variables = self.prepare_variables(variables, agent_config.variables) + else: + # FIXME: subagent input valid + variables = variables or {} # 2. 合并模型参数 effective_params = ModelParameterMerger.get_effective_parameters( @@ -588,95 +656,22 @@ class DraftRunService: # 4. 准备工具列表 tools = [] - tool_service = ToolService(self.db) tenant_id = ToolRepository.get_tenant_id_by_workspace_id(self.db, str(workspace_id)) # 从配置中获取启用的工具 - if hasattr(agent_config, 'tools') and agent_config.tools and isinstance(agent_config.tools, list): - for tool_config in agent_config.tools: - # print("+"*50) - # print(f"agent_config:{agent_config}") - # print(f"tool_config:{tool_config}") - if tool_config.get("enabled", False): - # 根据工具名称查找工具实例 - tool_instance = tool_service._get_tool_instance(tool_config.get("tool_id", ""), tenant_id) - if tool_instance: - if tool_instance.name == "baidu_search_tool" and not web_search: - continue - # 转换为LangChain工具 - langchain_tool = tool_instance.to_langchain_tool(tool_config.get("operation", None)) - tools.append(langchain_tool) - elif hasattr(agent_config, 'tools') and agent_config.tools and isinstance(agent_config.tools, dict): - web_tools = agent_config.tools - web_search_choice = web_tools.get("web_search", {}) - web_search_enable = web_search_choice.get("enabled", False) - if web_search: - if web_search_enable: - search_tool = create_web_search_tool({}) - tools.append(search_tool) + tools.extend(self.load_tools_config(tools_config, web_search, tenant_id)) + skill_tools, skill_prompts = self.load_skill_config(skills_config, message, tenant_id) + tools.extend(skill_tools) + if skill_prompts: + system_prompt = f"{system_prompt}\n\n{skill_prompts}" + tools.extend(self.load_knowledge_retrieval_config(knowledge_retrieval_config, user_id)) - logger.debug( - "已添加网络搜索工具", - extra={ - "tool_count": len(tools) - } - ) - - # 加载技能关联的工具 - if hasattr(agent_config, 'skills') and agent_config.skills: - skills = agent_config.skills - skill_enable = skills.get("enabled", False) - if skill_enable: - middleware = AgentMiddleware(skills=skills) - skill_tools, skill_configs, tool_to_skill_map = middleware.load_skill_tools(self.db, tenant_id) - tools.extend(skill_tools) - logger.debug(f"已加载 {len(skill_tools)} 个技能工具") - - # 应用动态过滤 - if skill_configs: - tools, activated_skill_ids = middleware.filter_tools(tools, message, skill_configs, - tool_to_skill_map) - logger.debug(f"过滤后剩余 {len(tools)} 个工具") - active_prompts = AgentMiddleware.get_active_prompts( - activated_skill_ids, skill_configs - ) - system_prompt = f"{system_prompt}\n\n{active_prompts}" - - # 添加知识库检索工具 - if agent_config.knowledge_retrieval: - kb_config = agent_config.knowledge_retrieval - knowledge_bases = kb_config.get("knowledge_bases", []) - kb_ids = bool(knowledge_bases and knowledge_bases[0].get("kb_id")) - if kb_ids: - # 创建知识库检索工具 - kb_tool = create_knowledge_retrieval_tool(kb_config, kb_ids, user_id) - tools.append(kb_tool) - - logger.debug( - "已添加知识库检索工具", - extra={ - "kb_ids": kb_ids, - "tool_count": len(tools) - } - ) # 添加长期记忆工具 + memory_flag = False if memory: - if agent_config.memory and agent_config.memory.get("enabled"): - memory_flag = True - memory_config = agent_config.memory - if user_id: - # 创建长期记忆工具 - memory_tool = create_long_term_memory_tool(memory_config, user_id, storage_type, - user_rag_memory_id) - tools.append(memory_tool) - - logger.debug( - "已添加长期记忆工具", - extra={ - "user_id": user_id, - "tool_count": len(tools) - } - ) + memory_tools, memory_flag = self.load_memory_config(memory_config, user_id, storage_type, + user_rag_memory_id) + tools.extend(memory_tools) # 4. 创建 LangChain Agent agent = LangChainAgent( @@ -702,10 +697,10 @@ class DraftRunService: # 6. 加载历史消息 history = [] - if agent_config.memory and agent_config.memory.get("enabled"): + if memory_config and memory_config.get("enabled"): history = await self._load_conversation_history( conversation_id=conversation_id, - max_history=agent_config.memory.get("max_history", 10) + max_history=memory_config.get("max_history", 10) ) # 6. 处理多模态文件 @@ -763,7 +758,7 @@ class DraftRunService: }) # 10. 保存会话消息 - if not sub_agent and agent_config.memory and agent_config.memory.get("enabled"): + if not sub_agent and memory_config and memory_config.get("enabled"): await self._save_conversation_message( conversation_id=conversation_id, user_message=message, @@ -969,7 +964,6 @@ class DraftRunService: List[Dict]: 历史消息列表 """ try: - from app.services.conversation_service import ConversationService conversation_service = ConversationService(self.db) history = conversation_service.get_conversation_history( @@ -1489,6 +1483,15 @@ class DraftRunService: "conversation_id": returned_conversation_id, "content": chunk })) + + if event_type == "error" and event_data: + await event_queue.put(self._format_sse_event("model_error", { + "model_index": idx, + "model_config_id": model_config_id, + "label": model_label, + "conversation_id": returned_conversation_id, + "error": event_data.get("error", "未知错误") + })) except Exception as e: logger.warning(f"解析流式事件失败: {e}") finally: @@ -1673,41 +1676,3 @@ class DraftRunService: "total_time": sum(r.get("elapsed_time", 0) for r in results) } ) - - -async def draft_run( - db: Session, - *, - agent_config: AgentConfig, - model_config: ModelConfig, - message: str, - user_id: Optional[str] = None, - kb_ids: Optional[List[str]] = None, - similarity_threshold: float = 0.7, - top_k: int = 3 -) -> Dict[str, Any]: - """试运行 Agent(便捷函数) - - Args: - db: 数据库会话 - agent_config: Agent 配置 - model_config: 模型配置 - message: 用户消息 - user_id: 用户ID - kb_ids: 知识库ID列表 - similarity_threshold: 相似度阈值 - top_k: 检索返回的文档数量 - - Returns: - Dict: 包含 AI 回复和元数据的字典 - """ - service = DraftRunService(db) - return await service.run( - agent_config=agent_config, - model_config=model_config, - message=message, - user_id=user_id, - kb_ids=kb_ids, - similarity_threshold=similarity_threshold, - top_k=top_k - ) diff --git a/api/app/services/langchain_tool_server.py b/api/app/services/langchain_tool_server.py index f44e4cdc..2c151956 100644 --- a/api/app/services/langchain_tool_server.py +++ b/api/app/services/langchain_tool_server.py @@ -9,6 +9,8 @@ load_dotenv() # 读取web_search环境变量 web_search_value = os.getenv('web_search') + + def Search(query): url = "https://qianfan.baidubce.com/v2/ai_search/chat/completions" api_key = web_search_value @@ -18,23 +20,24 @@ def Search(query): "role": "user", "content": query } - ], #搜索输入 - "edition":"standard", #搜索版本。默认为standard。可选值:standard:完整版本。lite:标准版本,对召回规模和精排条数简化后的版本,时延表现更好,效果略弱于完整版。 - "search_source": "baidu_search_v2", #使用的搜索引擎版本 - "resource_type_filter": [{"type": "web","top_k": 20}], #支持设置网页、视频、图片、阿拉丁搜索模态,网页top_k最大取值为50,视频top_k最大为10,图片top_k最大为30,阿拉丁top_k最大为5 + ], # 搜索输入 + "edition": "standard", # 搜索版本。默认为standard。可选值:standard:完整版本。lite:标准版本,对召回规模和精排条数简化后的版本,时延表现更好,效果略弱于完整版。 + "search_source": "baidu_search_v2", # 使用的搜索引擎版本 + "resource_type_filter": [{"type": "web", "top_k": 20}], + # 支持设置网页、视频、图片、阿拉丁搜索模态,网页top_k最大取值为50,视频top_k最大为10,图片top_k最大为30,阿拉丁top_k最大为5 "search_filter": { "range": { "page_time": { - "gte": "now-1w/d", #时间查询参数,大于或等于 - "lt": "now/d", #时间查询参数,小于 - "gt": "", #时间查询参数,大于 - "lte": "" #时间查询参数,小于或等于 + "gte": "now-1w/d", # 时间查询参数,大于或等于 + "lt": "now/d", # 时间查询参数,小于 + "gt": "", # 时间查询参数,大于 + "lte": "" # 时间查询参数,小于或等于 } } }, - "block_websites":["tieba.baidu.com"], #需要屏蔽的站点列表 - "search_recency_filter":"week", #根据网页发布时间进行筛选,可填值为:week,month,semiyear,year - "enable_full_content":True #是否输出网页完整原文 + "block_websites": ["tieba.baidu.com"], # 需要屏蔽的站点列表 + "search_recency_filter": "week", # 根据网页发布时间进行筛选,可填值为:week,month,semiyear,year + "enable_full_content": True # 是否输出网页完整原文 }, ensure_ascii=False) headers = { 'Content-Type': 'application/json', @@ -42,10 +45,10 @@ def Search(query): } response = requests.request("POST", url, headers=headers, data=payload.encode("utf-8")).json() - content=[] + content = [] for i in response['references']: - title=i['title'] - snippet=i['snippet'] - content.append(title+';'+snippet) - content='。'.join(content) - return content \ No newline at end of file + title = i['title'] + snippet = i['snippet'] + content.append(title + ';' + snippet) + content = '。'.join(content) + return content diff --git a/api/app/services/multi_agent_orchestrator.py b/api/app/services/multi_agent_orchestrator.py index 650f639b..f42ee95a 100644 --- a/api/app/services/multi_agent_orchestrator.py +++ b/api/app/services/multi_agent_orchestrator.py @@ -123,11 +123,14 @@ class MultiAgentOrchestrator: user_id: 用户 ID variables: 变量参数 use_llm_routing: 是否使用 LLM 路由 + web_search: 是否启用网络搜索 + memory: 是否启用记忆功能 + storage_type: 存储类型 + user_rag_memory_id: 用户 RAG 记忆 ID Yields: SSE 格式的事件流 """ - import json start_time = time.time() @@ -200,7 +203,8 @@ class MultiAgentOrchestrator: except Exception as e: logger.error( "多 Agent 任务执行失败(流式)", - extra={"error": str(e), "mode": self._normalized_mode} + extra={"error": str(e), "mode": self._normalized_mode}, + exc_info=True ) # 发送错误事件 yield self._format_sse_event("error", { @@ -1267,7 +1271,7 @@ class MultiAgentOrchestrator: Yields: SSE 格式的事件流 """ - from app.services.draft_run_service import DraftRunService + from app.services.draft_run_service import AgentRunService # 获取模型配置 model_config = self.db.get(ModelConfig, agent_config.default_model_config_id) @@ -1278,7 +1282,7 @@ class MultiAgentOrchestrator: ) # 流式执行 Agent - draft_service = DraftRunService(self.db) + draft_service = AgentRunService(self.db) async for event in draft_service.run_stream( agent_config=agent_config, model_config=model_config, @@ -1320,7 +1324,7 @@ class MultiAgentOrchestrator: Returns: 执行结果 """ - from app.services.draft_run_service import DraftRunService + from app.services.draft_run_service import AgentRunService # 获取模型配置 model_config = self.db.get(ModelConfig, agent_config.default_model_config_id) @@ -1331,7 +1335,7 @@ class MultiAgentOrchestrator: ) # 执行 Agent - draft_service = DraftRunService(self.db) + draft_service = AgentRunService(self.db) result = await draft_service.run( agent_config=agent_config, model_config=model_config, @@ -1633,6 +1637,7 @@ class MultiAgentOrchestrator: self.memory = config_data.get("memory") self.variables = config_data.get("variables", []) self.tools = config_data.get("tools", {}) + self.skills = config_data.get("skills", {}) self.default_model_config_id = release.default_model_config_id return AgentConfigProxy(release, app, config_data) diff --git a/api/app/services/skill_service.py b/api/app/services/skill_service.py index 5eb80795..0b7de6cf 100644 --- a/api/app/services/skill_service.py +++ b/api/app/services/skill_service.py @@ -121,7 +121,7 @@ class SkillService: if skill and skill.is_active: # 加载技能关联的工具 for tool_config in skill.tools: - tool = tool_service._get_tool_instance(tool_config.get("tool_id", ""), tenant_id) + tool = tool_service.get_tool_instance(tool_config.get("tool_id", ""), tenant_id) if tool: langchain_tool = tool.to_langchain_tool(tool_config.get("operation", None)) tools.append(langchain_tool) diff --git a/api/app/services/tool_service.py b/api/app/services/tool_service.py index 2bb96e53..d2400ded 100644 --- a/api/app/services/tool_service.py +++ b/api/app/services/tool_service.py @@ -209,7 +209,7 @@ class ToolService: try: # 获取工具实例 - tool = self._get_tool_instance(tool_id, tenant_id) + tool = self.get_tool_instance(tool_id, tenant_id) if not tool: return ToolResult.error_result( error=f"工具不存在: {tool_id}", @@ -335,7 +335,7 @@ class ToolService: return [] # 获取工具实例 - tool_instance = self._get_tool_instance(str(config.id), config.tenant_id) + tool_instance = self.get_tool_instance(str(config.id), config.tenant_id) if not tool_instance: return [] @@ -792,7 +792,7 @@ class ToolService: """获取工具配置""" return self.tool_repo.find_by_id_and_tenant(self.db, uuid.UUID(tool_id), tenant_id) - def _get_tool_instance(self, tool_id: str, tenant_id: uuid.UUID) -> Optional[BaseTool]: + def get_tool_instance(self, tool_id: str, tenant_id: uuid.UUID) -> Optional[BaseTool]: """获取工具实例""" if tool_id in self._tool_cache: return self._tool_cache[tool_id] @@ -1416,7 +1416,7 @@ class ToolService: """测试内置工具连接""" try: # 获取工具实例 - tool_instance = self._get_tool_instance(str(config.id), config.tenant_id) + tool_instance = self.get_tool_instance(str(config.id), config.tenant_id) if not tool_instance: return {"success": False, "message": "无法创建工具实例"} From a72d5d2c7768e090793172b7165e4a9f5ab2430e Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Thu, 5 Mar 2026 11:18:48 +0800 Subject: [PATCH 53/83] fix(workflow): add backward compatibility for old dify configs --- api/app/core/workflow/adapters/dify/converter.py | 6 ++++-- api/app/core/workflow/adapters/dify/dify_adapter.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/app/core/workflow/adapters/dify/converter.py b/api/app/core/workflow/adapters/dify/converter.py index 2014b4c3..06c988d3 100644 --- a/api/app/core/workflow/adapters/dify/converter.py +++ b/api/app/core/workflow/adapters/dify/converter.py @@ -98,7 +98,7 @@ class DifyConverter(BaseConverter): if not var_selector: return "" selector = var_selector.split('.') - if len(selector) not in [2, 3]: + if len(selector) not in [2, 3] and var_selector != "context": raise Exception(f"invalid variable selector: {var_selector}") if len(selector) == 3: selector = selector[1:] @@ -332,7 +332,9 @@ class DifyConverter(BaseConverter): messages.append( MessageConfig( role="user", - content=self.trans_variable_format(node_data["memory"]["query_prompt_template"]) + content=self.trans_variable_format( + node_data["memory"].get("query_prompt_template", "{{#sys.query#}}") + ) ) ) vision = node_data["vision"]["enabled"] diff --git a/api/app/core/workflow/adapters/dify/dify_adapter.py b/api/app/core/workflow/adapters/dify/dify_adapter.py index dcd14c7f..6336b1f9 100644 --- a/api/app/core/workflow/adapters/dify/dify_adapter.py +++ b/api/app/core/workflow/adapters/dify/dify_adapter.py @@ -80,7 +80,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter): return True def validate_config(self) -> bool: - require_fields = frozenset({'app', 'dependencies', 'kind', 'version', 'workflow'}) + require_fields = frozenset({'app', 'kind', 'version', 'workflow'}) if not all(field in self.config for field in require_fields): return False From 78ce2a9a8b557082c454fba875165db39ae05749 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Thu, 5 Mar 2026 14:07:27 +0800 Subject: [PATCH 54/83] feat(workflow): support multimodal input --- api/app/core/workflow/engine/variable_pool.py | 34 ++++++++++----- api/app/core/workflow/nodes/base_node.py | 41 ++++++++++++++----- .../workflow/nodes/cycle_graph/iteration.py | 4 +- .../core/workflow/nodes/cycle_graph/loop.py | 2 +- api/app/core/workflow/nodes/llm/node.py | 6 +-- .../core/workflow/variable/base_variable.py | 12 ++++-- .../workflow/variable/variable_objects.py | 7 +++- api/app/services/draft_run_service.py | 2 +- api/app/services/workflow_service.py | 14 ++++--- 9 files changed, 84 insertions(+), 38 deletions(-) diff --git a/api/app/core/workflow/engine/variable_pool.py b/api/app/core/workflow/engine/variable_pool.py index d08f47e5..bc88df19 100644 --- a/api/app/core/workflow/engine/variable_pool.py +++ b/api/app/core/workflow/engine/variable_pool.py @@ -303,38 +303,52 @@ class VariablePool: """ return self._get_variable_struct(selector) is not None - def get_all_system_vars(self) -> dict[str, Any]: + def get_all_system_vars(self, literal=False) -> dict[str, Any]: """获取所有系统变量 Returns: 系统变量字典 """ sys_namespace = self.variables.get("sys", {}) + if literal: + return {k: v.instance.to_literal() for k, v in sys_namespace.items()} return {k: v.instance.get_value() for k, v in sys_namespace.items()} - def get_all_conversation_vars(self) -> dict[str, Any]: + def get_all_conversation_vars(self, literal=False) -> dict[str, Any]: """获取所有会话变量 Returns: 会话变量字典 """ conv_namespace = self.variables.get("conv", {}) + if literal: + return {k: v.instance.to_literal() for k, v in conv_namespace.items()} return {k: v.instance.get_value() for k, v in conv_namespace.items()} - def get_all_node_outputs(self) -> dict[str, Any]: + def get_all_node_outputs(self, literal=False) -> dict[str, Any]: """获取所有节点输出(运行时变量) Returns: 节点输出字典,键为节点 ID """ - runtime_vars = { - namespace: { - k: v.instance.get_value() - for k, v in vars_dict.items() + if literal: + runtime_vars = { + namespace: { + k: v.instance.to_literal() + for k, v in vars_dict.items() + } + for namespace, vars_dict in self.variables.items() + if namespace not in ("sys", "conv") + } + else: + runtime_vars = { + namespace: { + k: v.instance.get_value() + for k, v in vars_dict.items() + } + for namespace, vars_dict in self.variables.items() + if namespace not in ("sys", "conv") } - for namespace, vars_dict in self.variables.items() - if namespace not in ("sys", "conv") - } return runtime_vars def get_node_output(self, node_id: str, defalut: Any = None, strict: bool = True) -> dict[str, Any] | None: diff --git a/api/app/core/workflow/nodes/base_node.py b/api/app/core/workflow/nodes/base_node.py index 3e30c00e..3f30718c 100644 --- a/api/app/core/workflow/nodes/base_node.py +++ b/api/app/core/workflow/nodes/base_node.py @@ -1,5 +1,6 @@ import asyncio import logging +import uuid from abc import ABC, abstractmethod from functools import cached_property from typing import Any, AsyncGenerator @@ -10,8 +11,10 @@ from app.core.config import settings from app.core.workflow.engine.state_manager import WorkflowState from app.core.workflow.engine.variable_pool import VariablePool from app.core.workflow.nodes.enums import BRANCH_NODES -from app.core.workflow.variable.base_variable import VariableType -from app.services.multimodal_service import PROVIDER_STRATEGIES +from app.core.workflow.variable.base_variable import VariableType, FileObject +from app.db import get_db_read +from app.schemas import FileInput +from app.services.multimodal_service import MultimodalService logger = logging.getLogger(__name__) @@ -548,9 +551,9 @@ class BaseNode(ABC): return render_template( template=template, - conv_vars=variable_pool.get_all_conversation_vars(), - node_outputs=variable_pool.get_all_node_outputs(), - system_vars=variable_pool.get_all_system_vars(), + conv_vars=variable_pool.get_all_conversation_vars(literal=True), + node_outputs=variable_pool.get_all_node_outputs(literal=True), + system_vars=variable_pool.get_all_system_vars(literal=True), strict=strict ) @@ -614,16 +617,32 @@ class BaseNode(ABC): return variable_pool.has(selector) @staticmethod - async def process_message(provider, content, enable_file=False) -> dict | str | None: + async def process_message(provider: str, content: str | FileObject, enable_file=False) -> dict | str | None: if isinstance(content, str): if enable_file: return {"text": content} return content - elif isinstance(content, dict): - trans_tool = PROVIDER_STRATEGIES[provider]() - result = await trans_tool.format_image(content["url"]) - return result - raise TypeError('Unexpect input value type') + + elif isinstance(content, FileObject): + if content.content_cache.get(provider): + return content.content_cache[provider] + with get_db_read() as db: + multimodel_service = MultimodalService(db, provider) + message = await multimodel_service.process_files( + [FileInput.model_construct( + type=content.type, + url=content.url, + transfer_method=content.transfer_method, + file_type=content.origin_file_type, + upload_file_id=content.file_id + )] + ) + + if message: + content.content_cache[provider] = message[0] + return message[0] + return None + raise TypeError(f'Unexpect input value type - {type(content)}') @staticmethod def process_model_output(content) -> str: diff --git a/api/app/core/workflow/nodes/cycle_graph/iteration.py b/api/app/core/workflow/nodes/cycle_graph/iteration.py index e4026f2d..cf7ac976 100644 --- a/api/app/core/workflow/nodes/cycle_graph/iteration.py +++ b/api/app/core/workflow/nodes/cycle_graph/iteration.py @@ -91,8 +91,8 @@ class IterationRuntime: return loopstate def merge_conv_vars(self): - self.variable_pool.get_all_conversation_vars().update( - self.child_variable_pool.get_all_conversation_vars() + self.variable_pool.variables["conv"].update( + self.child_variable_pool.variables["conv"] ) async def run_task(self, item, idx): diff --git a/api/app/core/workflow/nodes/cycle_graph/loop.py b/api/app/core/workflow/nodes/cycle_graph/loop.py index cebadfdc..d3ada1ec 100644 --- a/api/app/core/workflow/nodes/cycle_graph/loop.py +++ b/api/app/core/workflow/nodes/cycle_graph/loop.py @@ -156,7 +156,7 @@ class LoopRuntime: def merge_conv_vars(self, loopstate): self.variable_pool.variables["conv"].update( - self.child_variable_pool.variables.get("conv", {}) + self.child_variable_pool.variables["conv"] ) loop_vars = self.child_variable_pool.get_node_output(self.node_id, defalut={}, strict=False) loopstate["node_outputs"][self.node_id] = loop_vars diff --git a/api/app/core/workflow/nodes/llm/node.py b/api/app/core/workflow/nodes/llm/node.py index fdd5df58..c109d59b 100644 --- a/api/app/core/workflow/nodes/llm/node.py +++ b/api/app/core/workflow/nodes/llm/node.py @@ -172,9 +172,9 @@ class LLMNode(BaseNode): if self.typed_config.vision_input and self.typed_config.vision: file_content = [] - files = variable_pool.get_value(self.typed_config.vision_input) - for file in files: - content = await self.process_message(provider, file, self.typed_config.vision) + files = variable_pool.get_instance(self.typed_config.vision_input) + for file in files.value: + content = await self.process_message(provider, file.value, self.typed_config.vision) if content: file_content.append(content) if messages and messages[-1]["role"] == 'user': diff --git a/api/app/core/workflow/variable/base_variable.py b/api/app/core/workflow/variable/base_variable.py index 19cbdc74..dd821ea7 100644 --- a/api/app/core/workflow/variable/base_variable.py +++ b/api/app/core/workflow/variable/base_variable.py @@ -2,7 +2,7 @@ from enum import StrEnum from abc import abstractmethod, ABC from typing import Any -from pydantic import BaseModel +from pydantic import BaseModel, Field from app.schemas import FileType @@ -45,7 +45,7 @@ class VariableType(StrEnum): return cls.NUMBER elif isinstance(var, bool): return cls.BOOLEAN - elif isinstance(var, FileObject) or (isinstance(var, dict) and var.get('__file')): + elif isinstance(var, FileObject) or (isinstance(var, dict) and var.get('is_file')): return cls.FILE elif isinstance(var, dict): return cls.OBJECT @@ -109,7 +109,13 @@ def DEFAULT_VALUE(var_type: VariableType) -> Any: class FileObject(BaseModel): type: FileType url: str - __file: bool + transfer_method: str + origin_file_type: str + file_id: str | None + + content_cache: dict = Field(default_factory=dict) + + is_file: bool class BaseVariable(ABC): diff --git a/api/app/core/workflow/variable/variable_objects.py b/api/app/core/workflow/variable/variable_objects.py index 49541afc..63437fd9 100644 --- a/api/app/core/workflow/variable/variable_objects.py +++ b/api/app/core/workflow/variable/variable_objects.py @@ -63,13 +63,16 @@ class FileVariable(BaseVariable): def valid_value(self, value) -> FileObject: if isinstance(value, dict): - if not value.get("__file"): + if not value.get("is_file"): raise TypeError(f"Value must be a FileObject - {type(value)}:{value}") return FileObject( **{ "type": str(value.get('type')), + "transfer_method": value.get("transfer_method"), "url": value.get('url'), - "__file": True + "file_id": value.get("file_id"), + "origin_file_type": value.get("origin_file_type"), + "is_file": True } ) if isinstance(value, FileObject): diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 0cf68be2..bb68c815 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -255,7 +255,7 @@ class AgentRunService: @staticmethod def prepare_variables( input_vars: dict | None, - variables_config: dict | None + variables_config: dict ) -> dict: input_vars = input_vars or {} for variable in variables_config: diff --git a/api/app/services/workflow_service.py b/api/app/services/workflow_service.py index 02819efb..d13e3454 100644 --- a/api/app/services/workflow_service.py +++ b/api/app/services/workflow_service.py @@ -16,6 +16,7 @@ from app.core.workflow.adapters.registry import PlatformAdapterRegistry from app.core.workflow.executor import execute_workflow, execute_workflow_stream from app.core.workflow.nodes.enums import NodeType from app.core.workflow.validator import validate_workflow_config +from app.core.workflow.variable.base_variable import FileObject from app.db import get_db from app.models import App from app.models.workflow_model import WorkflowConfig, WorkflowExecution @@ -453,11 +454,14 @@ class WorkflowService: files_struct = [] for file in files: files_struct.append( - { - "type": file.type, - "url": await self.multimodal_service.get_file_url(file), - "__file": True - } + FileObject( + type=file.type, + url=await self.multimodal_service.get_file_url(file), + transfer_method=file.transfer_method, + file_id=str(file.upload_file_id), + origin_file_type=file.file_type, + is_file=True + ).model_dump() ) return files_struct From b5199b2eb91e90df116bdd430b9ffd9a971f769c Mon Sep 17 00:00:00 2001 From: lixiangcheng1 Date: Thu, 5 Mar 2026 14:18:33 +0800 Subject: [PATCH 55/83] =?UTF-8?q?=E3=80=90ADD=E3=80=91list=20operational?= =?UTF-8?q?=20mcp=20servers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mcp_market_config_controller.py | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/api/app/controllers/mcp_market_config_controller.py b/api/app/controllers/mcp_market_config_controller.py index 98012568..7f73663e 100644 --- a/api/app/controllers/mcp_market_config_controller.py +++ b/api/app/controllers/mcp_market_config_controller.py @@ -90,7 +90,7 @@ async def get_mcp_servers( cookies=cookies) raise_for_http_status(r) except requests.exceptions.RequestException as e: - api_logger.error(f"mFailed to get MCP servers: {str(e)}") + api_logger.error(f"Failed to get MCP servers: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get MCP servers: {str(e)}" @@ -118,6 +118,65 @@ async def get_mcp_servers( return success(data=result, msg="Query of mcp servers list successful") +@router.get("/operational_mcp_servers", response_model=ApiResponse) +async def get_operational_mcp_servers( + mcp_market_config_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Query the operational mcp servers list in pages + - Support keyword search for name,author,owner + - Return paging metadata + operational mcp server list + """ + api_logger.info( + f"Query operational mcp server list: tenant_id={current_user.tenant_id}, username: {current_user.username}") + + # 1. Query mcp market config information from the database + api_logger.debug(f"Query mcp market config: {mcp_market_config_id}") + db_mcp_market_config = mcp_market_config_service.get_mcp_market_config_by_id(db, + mcp_market_config_id=mcp_market_config_id, + current_user=current_user) + if not db_mcp_market_config: + api_logger.warning( + f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="The mcp market config does not exist or access is denied" + ) + + # 2. Execute paged query + api = MCPApi() + token = db_mcp_market_config.token + api.login(token) + + url = f'{api.mcp_base_url}/operational' + headers = api.builder_headers(api.headers) + + try: + cookies = api.get_cookies(access_token=token, cookies_required=True) + r = api.session.get(url, headers=headers, cookies=cookies) + raise_for_http_status(r) + except requests.exceptions.RequestException as e: + api_logger.error(f"Failed to get operational MCP servers: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get operational MCP servers: {str(e)}" + ) + + data = api._handle_response(r) + total = data.get('total_count', 0) + mcp_server_list = data.get('mcp_server_list', []) + # items = [{ + # 'name': item.get('name', ''), + # 'id': item.get('id', ''), + # 'description': item.get('description', '') + # } for item in mcp_server_list] + + # 3. Return structured response + return success(data=mcp_server_list, msg="Query of operational mcp servers list successful") + + @router.get("/mcp_server", response_model=ApiResponse) async def get_mcp_server( mcp_market_config_id: uuid.UUID, From 218637e81d29edb83c6c8d7204b2c291e6174c02 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 5 Mar 2026 14:42:42 +0800 Subject: [PATCH 56/83] [add] migration script --- .../versions/6a4641cf192b_202603051440.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 api/migrations/versions/6a4641cf192b_202603051440.py diff --git a/api/migrations/versions/6a4641cf192b_202603051440.py b/api/migrations/versions/6a4641cf192b_202603051440.py new file mode 100644 index 00000000..0322c9e2 --- /dev/null +++ b/api/migrations/versions/6a4641cf192b_202603051440.py @@ -0,0 +1,43 @@ +"""202603051440 + +Revision ID: 6a4641cf192b +Revises: b4af97639217 +Create Date: 2026-03-05 14:41:03.371557 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '6a4641cf192b' +down_revision: Union[str, None] = 'b4af97639217' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('implicit_emotions_storage', + sa.Column('id', sa.UUID(), nullable=False, comment='主键ID'), + sa.Column('end_user_id', sa.String(length=255), nullable=False, comment='终端用户ID'), + sa.Column('implicit_profile', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='隐性记忆用户画像数据'), + sa.Column('emotion_suggestions', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='情绪个性化建议数据'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.Column('implicit_generated_at', sa.DateTime(), nullable=True, comment='隐性记忆画像生成时间'), + sa.Column('emotion_generated_at', sa.DateTime(), nullable=True, comment='情绪建议生成时间'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('end_user_id') + ) + op.create_index('idx_updated_at', 'implicit_emotions_storage', ['updated_at'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('idx_updated_at', table_name='implicit_emotions_storage') + op.drop_table('implicit_emotions_storage') + # ### end Alembic commands ### From 60a95f655661e5cd0d22464ed63d943054c00759 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Thu, 5 Mar 2026 15:02:01 +0800 Subject: [PATCH 57/83] [changes] --- api/app/cache/__init__.py | 4 +--- api/app/cache/memory/__init__.py | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/api/app/cache/__init__.py b/api/app/cache/__init__.py index 5300348c..ca6a8784 100644 --- a/api/app/cache/__init__.py +++ b/api/app/cache/__init__.py @@ -4,10 +4,8 @@ Cache 缓存模块 提供各种缓存功能的统一入口 注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .memory import EmotionMemoryCache, ImplicitMemoryCache, InterestMemoryCache +from .memory import InterestMemoryCache __all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", "InterestMemoryCache", ] diff --git a/api/app/cache/memory/__init__.py b/api/app/cache/memory/__init__.py index 46ad0b73..7bc86068 100644 --- a/api/app/cache/memory/__init__.py +++ b/api/app/cache/memory/__init__.py @@ -4,12 +4,8 @@ Memory 缓存模块 提供记忆系统相关的缓存功能 注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .emotion_memory import EmotionMemoryCache -from .implicit_memory import ImplicitMemoryCache from .interest_memory import InterestMemoryCache __all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", "InterestMemoryCache", ] From 2e1eb9a5a67f2a041a3bb0b8efa02f0fad3af28d Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 15:12:18 +0800 Subject: [PATCH 58/83] feat(web): file type add default value --- web/src/views/Conversation/components/FileUpload.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index 98ece0e3..9da64cc7 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-03-04 18:54:47 + * @Last Modified time: 2026-03-05 15:09:22 */ /** * File Upload Component @@ -206,7 +206,7 @@ const UploadFiles = forwardRef(({ */ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { newFileList.map(file => { - const type = (file.type && transform_file_type[file.type as keyof typeof transform_file_type]) || file.type + const type = (file.type && transform_file_type[file.type as keyof typeof transform_file_type]) || file.type || 'document' file.type = type }) setFileList(newFileList); From 9c9fe9dde70873ac4d1c533f04ff6a0b72271de4 Mon Sep 17 00:00:00 2001 From: lanceyq <1982376970@qq.com> Date: Thu, 5 Mar 2026 16:21:27 +0800 Subject: [PATCH 59/83] [fix] Remove the unused ones --- api/app/cache/__init__.py | 5 +---- api/app/cache/memory/__init__.py | 5 ----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/api/app/cache/__init__.py b/api/app/cache/__init__.py index 5300348c..ca7aa91a 100644 --- a/api/app/cache/__init__.py +++ b/api/app/cache/__init__.py @@ -2,12 +2,9 @@ Cache 缓存模块 提供各种缓存功能的统一入口 -注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .memory import EmotionMemoryCache, ImplicitMemoryCache, InterestMemoryCache +from .memory import InterestMemoryCache __all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", "InterestMemoryCache", ] diff --git a/api/app/cache/memory/__init__.py b/api/app/cache/memory/__init__.py index 46ad0b73..9a7fd225 100644 --- a/api/app/cache/memory/__init__.py +++ b/api/app/cache/memory/__init__.py @@ -2,14 +2,9 @@ Memory 缓存模块 提供记忆系统相关的缓存功能 -注意:隐性记忆和情绪建议已迁移到数据库存储,不再使用Redis缓存 """ -from .emotion_memory import EmotionMemoryCache -from .implicit_memory import ImplicitMemoryCache from .interest_memory import InterestMemoryCache __all__ = [ - "EmotionMemoryCache", - "ImplicitMemoryCache", "InterestMemoryCache", ] From 621b074b3d6d2ee32e78cd9aaeb1521923103415 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 16:36:39 +0800 Subject: [PATCH 60/83] feat(web): memory config & ontology add default tag --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + web/src/views/MemoryManagement/index.tsx | 12 +++++++++--- web/src/views/MemoryManagement/types.ts | 3 ++- web/src/views/Ontology/index.tsx | 11 ++++++++--- web/src/views/Ontology/types.ts | 6 ++++-- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 7cef2d6c..e0b144a9 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -454,6 +454,7 @@ export const en = { prevStep: 'Previous Step', exportSuccess: 'Export successful', recommend: 'Recommend', + default: 'Default', }, model: { searchPlaceholder: 'search model…', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 5c688934..5306f711 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1030,6 +1030,7 @@ export const zh = { prevStep: '上一步', exportSuccess: '导出成功', recommend: '推荐', + default: '默认', }, model: { searchPlaceholder: '搜索模型…', diff --git a/web/src/views/MemoryManagement/index.tsx b/web/src/views/MemoryManagement/index.tsx index ac2b4fa5..6ebb49c7 100644 --- a/web/src/views/MemoryManagement/index.tsx +++ b/web/src/views/MemoryManagement/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 17:33:15 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 17:33:15 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-05 16:28:58 */ /** * Memory Management Page @@ -110,9 +110,15 @@ const MemoryManagement: React.FC = () => { + {item.is_system_default && +
+ {t('common.default')} +
+ } -
{item.config_desc}
+
{item.config_desc}
diff --git a/web/src/views/MemoryManagement/types.ts b/web/src/views/MemoryManagement/types.ts index 48bdbb77..dc3ae091 100644 --- a/web/src/views/MemoryManagement/types.ts +++ b/web/src/views/MemoryManagement/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 17:33:01 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 17:33:24 + * @Last Modified time: 2026-03-05 16:33:53 */ /** * Memory management form data type @@ -42,6 +42,7 @@ export interface Memory { workspace_id: string; scene_id: string; scene_name: string; + is_system_default: boolean; [key: string]: string | number | boolean; } /** diff --git a/web/src/views/Ontology/index.tsx b/web/src/views/Ontology/index.tsx index 37f9118d..eaf1188b 100644 --- a/web/src/views/Ontology/index.tsx +++ b/web/src/views/Ontology/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 14:10:15 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-05 10:57:53 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-05 16:28:53 */ import { type FC, useState, useRef, type MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -144,8 +144,13 @@ const Ontology: FC = () => { title={item.scene_name} extra={{item.type_num} {t('ontology.typeCount')}} onClick={() => handleJump(item)} - className="rb:cursor-pointer" + className="rb:cursor-pointer rb:relative" > + {item.is_system_default && +
+ {t('common.default')} +
+ }
diff --git a/web/src/views/Ontology/types.ts b/web/src/views/Ontology/types.ts index d78d8464..aad94ee0 100644 --- a/web/src/views/Ontology/types.ts +++ b/web/src/views/Ontology/types.ts @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 14:10:10 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 14:10:10 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-05 16:18:56 */ /** * Query parameters for ontology list pagination and filtering @@ -38,6 +38,8 @@ export interface OntologyItem { updated_at: number; /** Total count of classes in the scene */ classes_count: number; + /** Whether this is the system default configuration */ + is_system_default: boolean; } /** From 495c5802a0496a2177a30f31dbfa691bc193ef6a Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 5 Mar 2026 16:43:59 +0800 Subject: [PATCH 61/83] feat(web): knowledge add form rules --- web/src/views/KnowledgeBase/components/CreateModal.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/views/KnowledgeBase/components/CreateModal.tsx b/web/src/views/KnowledgeBase/components/CreateModal.tsx index 76640058..d9727d18 100644 --- a/web/src/views/KnowledgeBase/components/CreateModal.tsx +++ b/web/src/views/KnowledgeBase/components/CreateModal.tsx @@ -15,6 +15,7 @@ import { } from '@/api/knowledgeBase' import RbModal from '@/components/RbModal' import SliderInput from '@/components/SliderInput' +import { stringRegExp } from '@/utils/validator' const { TextArea } = Input; const { confirm } = Modal @@ -519,12 +520,16 @@ const CreateModal = forwardRef(({ )} - +