Merge branch 'refs/heads/develop' into fix/memory_bug_fix
This commit is contained in:
11
api/app/cache/__init__.py
vendored
Normal file
11
api/app/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Cache 缓存模块
|
||||
|
||||
提供各种缓存功能的统一入口
|
||||
"""
|
||||
from .memory import EmotionMemoryCache, ImplicitMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"EmotionMemoryCache",
|
||||
"ImplicitMemoryCache",
|
||||
]
|
||||
12
api/app/cache/memory/__init__.py
vendored
Normal file
12
api/app/cache/memory/__init__.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Memory 缓存模块
|
||||
|
||||
提供记忆系统相关的缓存功能
|
||||
"""
|
||||
from .emotion_memory import EmotionMemoryCache
|
||||
from .implicit_memory import ImplicitMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"EmotionMemoryCache",
|
||||
"ImplicitMemoryCache",
|
||||
]
|
||||
134
api/app/cache/memory/emotion_memory.py
vendored
Normal file
134
api/app/cache/memory/emotion_memory.py
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
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
|
||||
136
api/app/cache/memory/implicit_memory.py
vendored
Normal file
136
api/app/cache/memory/implicit_memory.py
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
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
|
||||
@@ -18,6 +18,7 @@ from app.models.user_model import User
|
||||
from app.schemas.emotion_schema import (
|
||||
EmotionHealthRequest,
|
||||
EmotionSuggestionsRequest,
|
||||
EmotionGenerateSuggestionsRequest,
|
||||
EmotionTagsRequest,
|
||||
EmotionWordcloudRequest,
|
||||
)
|
||||
@@ -58,7 +59,7 @@ async def get_emotion_tags(
|
||||
"limit": request.limit
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 调用服务层
|
||||
data = await emotion_service.get_emotion_tags(
|
||||
end_user_id=request.group_id,
|
||||
@@ -67,7 +68,7 @@ async def get_emotion_tags(
|
||||
end_date=request.end_date,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
|
||||
api_logger.info(
|
||||
"情绪标签统计获取成功",
|
||||
extra={
|
||||
@@ -76,9 +77,9 @@ async def get_emotion_tags(
|
||||
"tags_count": len(data.get("tags", []))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return success(data=data, msg="情绪标签获取成功")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"获取情绪标签统计失败: {str(e)}",
|
||||
@@ -107,14 +108,14 @@ async def get_emotion_wordcloud(
|
||||
"limit": request.limit
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 调用服务层
|
||||
data = await emotion_service.get_emotion_wordcloud(
|
||||
end_user_id=request.group_id,
|
||||
emotion_type=request.emotion_type,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
|
||||
api_logger.info(
|
||||
"情绪词云数据获取成功",
|
||||
extra={
|
||||
@@ -122,9 +123,9 @@ async def get_emotion_wordcloud(
|
||||
"total_keywords": data.get("total_keywords", 0)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return success(data=data, msg="情绪词云获取成功")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"获取情绪词云数据失败: {str(e)}",
|
||||
@@ -151,7 +152,7 @@ async def get_emotion_health(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="时间范围参数无效,必须是 7d、30d 或 90d"
|
||||
)
|
||||
|
||||
|
||||
api_logger.info(
|
||||
f"用户 {current_user.username} 请求获取情绪健康指数",
|
||||
extra={
|
||||
@@ -159,13 +160,13 @@ async def get_emotion_health(
|
||||
"time_range": request.time_range
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 调用服务层
|
||||
data = await emotion_service.calculate_emotion_health_index(
|
||||
end_user_id=request.group_id,
|
||||
time_range=request.time_range
|
||||
)
|
||||
|
||||
|
||||
api_logger.info(
|
||||
"情绪健康指数获取成功",
|
||||
extra={
|
||||
@@ -174,9 +175,9 @@ async def get_emotion_health(
|
||||
"level": data.get("level", "未知")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return success(data=data, msg="情绪健康指数获取成功")
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -198,15 +199,80 @@ async def get_emotion_suggestions(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取个性化情绪建议
|
||||
|
||||
"""获取个性化情绪建议(从缓存读取)
|
||||
|
||||
Args:
|
||||
request: 包含 group_id 和可选的 config_id
|
||||
db: 数据库会话
|
||||
current_user: 当前用户
|
||||
|
||||
|
||||
Returns:
|
||||
个性化情绪建议响应
|
||||
缓存的个性化情绪建议响应
|
||||
"""
|
||||
try:
|
||||
api_logger.info(
|
||||
f"用户 {current_user.username} 请求获取个性化情绪建议(缓存)",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"config_id": request.config_id
|
||||
}
|
||||
)
|
||||
|
||||
# 从缓存获取建议
|
||||
data = await emotion_service.get_cached_suggestions(
|
||||
end_user_id=request.group_id,
|
||||
db=db
|
||||
)
|
||||
|
||||
if data is None:
|
||||
# 缓存不存在或已过期
|
||||
api_logger.info(
|
||||
f"用户 {request.group_id} 的建议缓存不存在或已过期",
|
||||
extra={"group_id": request.group_id}
|
||||
)
|
||||
return fail(
|
||||
BizCode.NOT_FOUND,
|
||||
"建议缓存不存在或已过期,请调用 /generate_suggestions 接口生成新建议",
|
||||
""
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
"个性化建议获取成功(缓存)",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"suggestions_count": len(data.get("suggestions", []))
|
||||
}
|
||||
)
|
||||
|
||||
return success(data=data, msg="个性化建议获取成功(缓存)")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"获取个性化建议失败: {str(e)}",
|
||||
extra={"group_id": request.group_id},
|
||||
exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取个性化建议失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/generate_suggestions", response_model=ApiResponse)
|
||||
async def generate_emotion_suggestions(
|
||||
request: EmotionGenerateSuggestionsRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""生成个性化情绪建议(调用LLM并缓存)
|
||||
|
||||
Args:
|
||||
request: 包含 group_id、可选的 config_id 和 force_refresh
|
||||
db: 数据库会话
|
||||
current_user: 当前用户
|
||||
|
||||
Returns:
|
||||
新生成的个性化情绪建议响应
|
||||
"""
|
||||
try:
|
||||
# 验证 config_id(如果提供)
|
||||
@@ -232,38 +298,46 @@ async def get_emotion_suggestions(
|
||||
return fail(BizCode.INVALID_PARAMETER, "配置ID无效", f"配置 {config_id} 不存在")
|
||||
except Exception as e:
|
||||
return fail(BizCode.INVALID_PARAMETER, "配置ID验证失败", str(e))
|
||||
|
||||
|
||||
api_logger.info(
|
||||
f"用户 {current_user.username} 请求获取个性化情绪建议",
|
||||
f"用户 {current_user.username} 请求生成个性化情绪建议",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"config_id": config_id
|
||||
}
|
||||
)
|
||||
|
||||
# 调用服务层
|
||||
|
||||
# 调用服务层生成建议
|
||||
data = await emotion_service.generate_emotion_suggestions(
|
||||
end_user_id=request.group_id,
|
||||
db=db
|
||||
)
|
||||
|
||||
|
||||
# 保存到缓存
|
||||
await emotion_service.save_suggestions_cache(
|
||||
end_user_id=request.group_id,
|
||||
suggestions_data=data,
|
||||
db=db,
|
||||
expires_hours=24
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
"个性化建议获取成功",
|
||||
"个性化建议生成成功",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"suggestions_count": len(data.get("suggestions", []))
|
||||
}
|
||||
)
|
||||
|
||||
return success(data=data, msg="个性化建议获取成功")
|
||||
|
||||
|
||||
return success(data=data, msg="个性化建议生成成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"获取个性化建议失败: {str(e)}",
|
||||
f"生成个性化建议失败: {str(e)}",
|
||||
extra={"group_id": request.group_id},
|
||||
exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取个性化建议失败: {str(e)}"
|
||||
detail=f"生成个性化建议失败: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -33,5 +33,12 @@ def get_workspace_list(
|
||||
def get_system_version():
|
||||
"""获取系统版本号+说明"""
|
||||
current_version = settings.SYSTEM_VERSION
|
||||
version_introduction = HomePageService.load_version_introduction(current_version)
|
||||
return success(data={"version": current_version, "introduction": version_introduction}, msg="系统版本获取成功")
|
||||
version_info = HomePageService.load_version_introduction(current_version)
|
||||
return success(
|
||||
data={
|
||||
"version": current_version,
|
||||
"introduction": version_info.get("introduction"),
|
||||
"introduction_en": version_info.get("introduction_en")
|
||||
},
|
||||
msg="系统版本获取成功"
|
||||
)
|
||||
@@ -11,6 +11,7 @@ from app.dependencies import (
|
||||
)
|
||||
from app.models.user_model import User
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.schemas.implicit_memory_schema import GenerateProfileRequest
|
||||
from app.services.implicit_memory_service import ImplicitMemoryService
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -133,7 +134,7 @@ async def get_preference_tags(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> ApiResponse:
|
||||
"""
|
||||
Get user preference tags with filtering options.
|
||||
Get user preference tags from cache.
|
||||
|
||||
Args:
|
||||
user_id: Target user ID
|
||||
@@ -143,35 +144,56 @@ async def get_preference_tags(
|
||||
end_date: Optional end date filter
|
||||
|
||||
Returns:
|
||||
List of preference tags matching the filters
|
||||
List of preference tags from cache
|
||||
"""
|
||||
api_logger.info(f"Preference tags requested for user: {user_id}")
|
||||
api_logger.info(f"Preference tags requested for user: {user_id} (from cache)")
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
validate_user_id(user_id)
|
||||
validate_confidence_threshold(confidence_threshold)
|
||||
validate_date_range(start_date, end_date)
|
||||
|
||||
# Create service with user-specific config
|
||||
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
||||
|
||||
# Build date range
|
||||
date_range = None
|
||||
if start_date and end_date:
|
||||
from app.schemas.implicit_memory_schema import DateRange
|
||||
date_range = DateRange(start_date=start_date, end_date=end_date)
|
||||
# Get cached profile
|
||||
cached_profile = await service.get_cached_profile(end_user_id=user_id, db=db)
|
||||
|
||||
# Get preference tags
|
||||
tags = await service.get_preference_tags(
|
||||
user_id=user_id,
|
||||
confidence_threshold=confidence_threshold,
|
||||
tag_category=tag_category,
|
||||
date_range=date_range
|
||||
)
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
""
|
||||
)
|
||||
|
||||
api_logger.info(f"Retrieved {len(tags)} preference tags for user: {user_id}")
|
||||
return success(data=[tag.model_dump(mode='json') for tag in tags], msg="偏好标签获取成功")
|
||||
# Extract preferences from cache
|
||||
preferences = cached_profile.get("preferences", [])
|
||||
|
||||
# Apply filters (client-side filtering on cached data)
|
||||
filtered_preferences = []
|
||||
for pref in preferences:
|
||||
# Filter by confidence threshold
|
||||
if confidence_threshold is not None and pref.get("confidence_score", 0) < confidence_threshold:
|
||||
continue
|
||||
|
||||
# Filter by category if specified
|
||||
if tag_category and pref.get("category") != tag_category:
|
||||
continue
|
||||
|
||||
# Filter by date range if specified
|
||||
if start_date or end_date:
|
||||
created_at_ts = pref.get("created_at")
|
||||
if created_at_ts:
|
||||
created_at = datetime.fromtimestamp(created_at_ts / 1000)
|
||||
if start_date and created_at < start_date:
|
||||
continue
|
||||
if end_date and created_at > end_date:
|
||||
continue
|
||||
|
||||
filtered_preferences.append(pref)
|
||||
|
||||
api_logger.info(f"Retrieved {len(filtered_preferences)} preference tags for user: {user_id} (from cache)")
|
||||
return success(data=filtered_preferences, msg="偏好标签获取成功(缓存)")
|
||||
|
||||
except Exception as e:
|
||||
return handle_implicit_memory_error(e, "偏好标签获取", user_id)
|
||||
@@ -186,16 +208,16 @@ async def get_dimension_portrait(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> ApiResponse:
|
||||
"""
|
||||
Get user's four-dimension personality portrait.
|
||||
Get user's four-dimension personality portrait from cache.
|
||||
|
||||
Args:
|
||||
user_id: Target user ID
|
||||
include_history: Whether to include historical trend data
|
||||
include_history: Whether to include historical trend data (ignored for cached data)
|
||||
|
||||
Returns:
|
||||
Four-dimension personality portrait with scores and evidence
|
||||
Four-dimension personality portrait from cache
|
||||
"""
|
||||
api_logger.info(f"Dimension portrait requested for user: {user_id}")
|
||||
api_logger.info(f"Dimension portrait requested for user: {user_id} (from cache)")
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
@@ -204,13 +226,22 @@ async def get_dimension_portrait(
|
||||
# Create service with user-specific config
|
||||
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
||||
|
||||
portrait = await service.get_dimension_portrait(
|
||||
user_id=user_id,
|
||||
include_history=include_history
|
||||
)
|
||||
# Get cached profile
|
||||
cached_profile = await service.get_cached_profile(end_user_id=user_id, db=db)
|
||||
|
||||
api_logger.info(f"Dimension portrait retrieved for user: {user_id}")
|
||||
return success(data=portrait.model_dump(mode='json'), msg="四维画像获取成功")
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
""
|
||||
)
|
||||
|
||||
# Extract portrait from cache
|
||||
portrait = cached_profile.get("portrait", {})
|
||||
|
||||
api_logger.info(f"Dimension portrait retrieved for user: {user_id} (from cache)")
|
||||
return success(data=portrait, msg="四维画像获取成功(缓存)")
|
||||
|
||||
except Exception as e:
|
||||
return handle_implicit_memory_error(e, "四维画像获取", user_id)
|
||||
@@ -225,16 +256,16 @@ async def get_interest_area_distribution(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> ApiResponse:
|
||||
"""
|
||||
Get user's interest area distribution across four areas.
|
||||
Get user's interest area distribution from cache.
|
||||
|
||||
Args:
|
||||
user_id: Target user ID
|
||||
include_trends: Whether to include trend analysis data
|
||||
include_trends: Whether to include trend analysis data (ignored for cached data)
|
||||
|
||||
Returns:
|
||||
Interest area distribution with percentages and evidence
|
||||
Interest area distribution from cache
|
||||
"""
|
||||
api_logger.info(f"Interest area distribution requested for user: {user_id}")
|
||||
api_logger.info(f"Interest area distribution requested for user: {user_id} (from cache)")
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
@@ -243,13 +274,22 @@ async def get_interest_area_distribution(
|
||||
# Create service with user-specific config
|
||||
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
||||
|
||||
distribution = await service.get_interest_area_distribution(
|
||||
user_id=user_id,
|
||||
include_trends=include_trends
|
||||
)
|
||||
# Get cached profile
|
||||
cached_profile = await service.get_cached_profile(end_user_id=user_id, db=db)
|
||||
|
||||
api_logger.info(f"Interest area distribution retrieved for user: {user_id}")
|
||||
return success(data=distribution.model_dump(mode='json'), msg="兴趣领域分布获取成功")
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
""
|
||||
)
|
||||
|
||||
# Extract interest areas from cache
|
||||
interest_areas = cached_profile.get("interest_areas", {})
|
||||
|
||||
api_logger.info(f"Interest area distribution retrieved for user: {user_id} (from cache)")
|
||||
return success(data=interest_areas, msg="兴趣领域分布获取成功(缓存)")
|
||||
|
||||
except Exception as e:
|
||||
return handle_implicit_memory_error(e, "兴趣领域分布获取", user_id)
|
||||
@@ -266,7 +306,7 @@ async def get_behavior_habits(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> ApiResponse:
|
||||
"""
|
||||
Get user's behavioral habits with filtering options.
|
||||
Get user's behavioral habits from cache.
|
||||
|
||||
Args:
|
||||
user_id: Target user ID
|
||||
@@ -275,38 +315,117 @@ async def get_behavior_habits(
|
||||
time_period: Filter by time period (current, past)
|
||||
|
||||
Returns:
|
||||
List of behavioral habits matching the filters
|
||||
List of behavioral habits from cache
|
||||
"""
|
||||
api_logger.info(f"Behavior habits requested for user: {user_id}")
|
||||
api_logger.info(f"Behavior habits requested for user: {user_id} (from cache)")
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
validate_user_id(user_id)
|
||||
|
||||
# Convert string confidence level to numerical
|
||||
numerical_confidence = None
|
||||
if confidence_level:
|
||||
confidence_mapping = {
|
||||
"high": 85,
|
||||
"medium": 50,
|
||||
"low": 20
|
||||
}
|
||||
numerical_confidence = confidence_mapping.get(confidence_level.lower())
|
||||
|
||||
# Create service with user-specific config
|
||||
service = ImplicitMemoryService(db=db, end_user_id=user_id)
|
||||
|
||||
habits = await service.get_behavior_habits(
|
||||
user_id=user_id,
|
||||
confidence_level=numerical_confidence,
|
||||
frequency_pattern=frequency_pattern,
|
||||
time_period=time_period
|
||||
)
|
||||
# Get cached profile
|
||||
cached_profile = await service.get_cached_profile(end_user_id=user_id, db=db)
|
||||
|
||||
api_logger.info(f"Retrieved {len(habits)} behavior habits for user: {user_id}")
|
||||
return success(data=[habit.model_dump(mode='json') for habit in habits], msg="行为习惯获取成功")
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
""
|
||||
)
|
||||
|
||||
# Extract habits from cache
|
||||
habits = cached_profile.get("habits", [])
|
||||
|
||||
# Apply filters (client-side filtering on cached data)
|
||||
filtered_habits = []
|
||||
for habit in habits:
|
||||
# Filter by confidence level
|
||||
if confidence_level:
|
||||
confidence_mapping = {
|
||||
"high": 85,
|
||||
"medium": 50,
|
||||
"low": 20
|
||||
}
|
||||
numerical_confidence = confidence_mapping.get(confidence_level.lower())
|
||||
if habit.get("confidence_level", 0) < numerical_confidence:
|
||||
continue
|
||||
|
||||
# Filter by frequency pattern
|
||||
if frequency_pattern and habit.get("frequency_pattern") != frequency_pattern:
|
||||
continue
|
||||
|
||||
# Filter by time period
|
||||
if time_period:
|
||||
is_current = habit.get("is_current", True)
|
||||
if time_period.lower() == "current" and not is_current:
|
||||
continue
|
||||
elif time_period.lower() == "past" and is_current:
|
||||
continue
|
||||
|
||||
filtered_habits.append(habit)
|
||||
|
||||
api_logger.info(f"Retrieved {len(filtered_habits)} behavior habits for user: {user_id} (from cache)")
|
||||
return success(data=filtered_habits, msg="行为习惯获取成功(缓存)")
|
||||
|
||||
except Exception as e:
|
||||
return handle_implicit_memory_error(e, "行为习惯获取", user_id)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/generate_profile", response_model=ApiResponse)
|
||||
@cur_workspace_access_guard()
|
||||
async def generate_implicit_memory_profile(
|
||||
request: GenerateProfileRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> ApiResponse:
|
||||
"""
|
||||
Generate complete user profile (all 4 modules) and cache it.
|
||||
|
||||
Args:
|
||||
request: Generate profile request with end_user_id
|
||||
db: Database session
|
||||
current_user: Current authenticated user
|
||||
|
||||
Returns:
|
||||
Complete user profile with all modules
|
||||
"""
|
||||
end_user_id = request.end_user_id
|
||||
api_logger.info(f"Generate profile requested for user: {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)
|
||||
|
||||
# Generate complete profile (calls LLM for all 4 modules)
|
||||
api_logger.info(f"开始生成完整用户画像: user={end_user_id}")
|
||||
profile_data = await service.generate_complete_profile(user_id=end_user_id)
|
||||
|
||||
# Save to cache
|
||||
await service.save_profile_cache(
|
||||
end_user_id=end_user_id,
|
||||
profile_data=profile_data,
|
||||
db=db,
|
||||
expires_hours=168 # 7 days
|
||||
)
|
||||
|
||||
api_logger.info(f"用户画像生成并缓存成功: user={end_user_id}")
|
||||
|
||||
# Add metadata
|
||||
profile_data["end_user_id"] = end_user_id
|
||||
profile_data["cached"] = False
|
||||
|
||||
return success(data=profile_data, msg="用户画像生成成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"生成用户画像失败: user={end_user_id}, error={str(e)}", exc_info=True)
|
||||
return handle_implicit_memory_error(e, "用户画像生成", end_user_id)
|
||||
|
||||
@@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.core.response_utils import success
|
||||
from app.db import get_db
|
||||
from app.db import get_db, get_db_read
|
||||
from app.dependencies import get_share_user_id, ShareTokenData
|
||||
from app.repositories import knowledge_repository
|
||||
from app.repositories.workflow_repository import WorkflowConfigRepository
|
||||
from app.schemas import release_share_schema, conversation_schema
|
||||
from app.schemas.response_schema import PageData, PageMeta
|
||||
from app.services import workspace_service
|
||||
@@ -19,7 +20,8 @@ from app.services.conversation_service import ConversationService
|
||||
from app.services.release_share_service import ReleaseShareService
|
||||
from app.services.shared_chat_service import SharedChatService
|
||||
from app.services.app_chat_service import AppChatService, get_app_chat_service
|
||||
from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, agent_config_4_app_release, multi_agent_config_4_app_release
|
||||
from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, \
|
||||
agent_config_4_app_release, multi_agent_config_4_app_release
|
||||
|
||||
router = APIRouter(prefix="/public/share", tags=["Public Share"])
|
||||
logger = get_business_logger()
|
||||
@@ -65,10 +67,10 @@ def get_or_generate_user_id(payload_user_id: str, request: Request) -> str:
|
||||
summary="获取访问 token"
|
||||
)
|
||||
def get_access_token(
|
||||
share_token: str,
|
||||
payload: release_share_schema.TokenRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
share_token: str,
|
||||
payload: release_share_schema.TokenRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取访问 token
|
||||
|
||||
@@ -113,9 +115,9 @@ def get_access_token(
|
||||
response_model=None
|
||||
)
|
||||
def get_shared_release(
|
||||
password: str = Query(None, description="访问密码(如果需要)"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Query(None, description="访问密码(如果需要)"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取公开分享的发布版本信息
|
||||
|
||||
@@ -137,9 +139,9 @@ def get_shared_release(
|
||||
summary="验证访问密码"
|
||||
)
|
||||
def verify_password(
|
||||
payload: release_share_schema.PasswordVerifyRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
payload: release_share_schema.PasswordVerifyRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""验证分享的访问密码
|
||||
|
||||
@@ -159,11 +161,11 @@ def verify_password(
|
||||
summary="获取嵌入代码"
|
||||
)
|
||||
def get_embed_code(
|
||||
width: str = Query("100%", description="iframe 宽度"),
|
||||
height: str = Query("600px", description="iframe 高度"),
|
||||
request: Request = None,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
width: str = Query("100%", description="iframe 宽度"),
|
||||
height: str = Query("600px", description="iframe 高度"),
|
||||
request: Request = None,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取嵌入代码
|
||||
|
||||
@@ -183,7 +185,6 @@ def get_embed_code(
|
||||
return success(data=embed_code)
|
||||
|
||||
|
||||
|
||||
# ---------- 会话管理接口 ----------
|
||||
|
||||
@router.get(
|
||||
@@ -191,11 +192,11 @@ def get_embed_code(
|
||||
summary="获取会话列表"
|
||||
)
|
||||
def list_conversations(
|
||||
password: str = Query(None, description="访问密码"),
|
||||
page: int = Query(1, ge=1),
|
||||
pagesize: int = Query(20, ge=1, le=100),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Query(None, description="访问密码"),
|
||||
page: int = Query(1, ge=1),
|
||||
pagesize: int = Query(20, ge=1, le=100),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取分享应用的会话列表
|
||||
|
||||
@@ -209,9 +210,9 @@ def list_conversations(
|
||||
from app.repositories.end_user_repository import EndUserRepository
|
||||
end_user_repo = EndUserRepository(db)
|
||||
new_end_user = end_user_repo.get_or_create_end_user(
|
||||
app_id=share.app_id,
|
||||
other_id=other_id
|
||||
)
|
||||
app_id=share.app_id,
|
||||
other_id=other_id
|
||||
)
|
||||
logger.debug(new_end_user.id)
|
||||
service = SharedChatService(db)
|
||||
conversations, total = service.list_conversations(
|
||||
@@ -233,10 +234,10 @@ def list_conversations(
|
||||
summary="获取会话详情(含消息)"
|
||||
)
|
||||
def get_conversation(
|
||||
conversation_id: uuid.UUID,
|
||||
password: str = Query(None, description="访问密码"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
conversation_id: uuid.UUID,
|
||||
password: str = Query(None, description="访问密码"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取会话详情和消息历史"""
|
||||
chat_service = SharedChatService(db)
|
||||
@@ -266,10 +267,10 @@ def get_conversation(
|
||||
summary="发送消息(支持流式和非流式)"
|
||||
)
|
||||
async def chat(
|
||||
payload: conversation_schema.ChatRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
|
||||
payload: conversation_schema.ChatRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
|
||||
):
|
||||
"""发送消息并获取回复
|
||||
|
||||
@@ -313,7 +314,7 @@ async def chat(
|
||||
)
|
||||
end_user_id = str(new_end_user.id)
|
||||
|
||||
appid=share.app_id
|
||||
appid = share.app_id
|
||||
"""获取存储类型和工作空间的ID"""
|
||||
|
||||
# 直接通过 SQLAlchemy 查询 app
|
||||
@@ -425,16 +426,16 @@ async def chat(
|
||||
# )
|
||||
async def event_generator():
|
||||
async for event in app_chat_service.agnet_chat_stream(
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id= str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
web_search=payload.web_search,
|
||||
config=agent_config,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
workspace_id=workspace_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
web_search=payload.web_search,
|
||||
config=agent_config,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
workspace_id=workspace_id
|
||||
):
|
||||
yield event
|
||||
|
||||
@@ -481,15 +482,15 @@ async def chat(
|
||||
async def event_generator():
|
||||
async for event in app_chat_service.multi_agent_chat_stream(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id
|
||||
):
|
||||
yield event
|
||||
|
||||
@@ -561,24 +562,27 @@ async def chat(
|
||||
|
||||
# return success(data=conversation_schema.ChatResponse(**result))
|
||||
elif app_type == AppType.WORKFLOW:
|
||||
|
||||
config = workflow_config_4_app_release(release)
|
||||
if not config.id:
|
||||
with get_db_read() as db:
|
||||
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
|
||||
config.id = source_config.id
|
||||
config.id = uuid.UUID(config.id)
|
||||
if payload.stream:
|
||||
async def event_generator():
|
||||
|
||||
async for event in app_chat_service.workflow_chat_stream(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release.id
|
||||
):
|
||||
event_type = event.get("event", "message")
|
||||
event_data = event.get("data", {})
|
||||
@@ -610,7 +614,8 @@ async def chat(
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id
|
||||
workspace_id=workspace_id,
|
||||
release_id=release.id
|
||||
)
|
||||
logger.debug(
|
||||
"工作流试运行返回结果",
|
||||
|
||||
@@ -242,8 +242,9 @@ async def chat(
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.app_id,
|
||||
workspace_id=workspace_id
|
||||
app_id=app.id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=app.current_release.id,
|
||||
):
|
||||
event_type = event.get("event", "message")
|
||||
event_data = event.get("data", {})
|
||||
@@ -274,8 +275,9 @@ async def chat(
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.app_id,
|
||||
workspace_id=workspace_id
|
||||
app_id=app.id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=app.current_release.id
|
||||
)
|
||||
logger.debug(
|
||||
"工作流试运行返回结果",
|
||||
|
||||
@@ -38,6 +38,7 @@ class Settings:
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "1"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||
|
||||
|
||||
# ElasticSearch configuration
|
||||
ELASTICSEARCH_HOST: str = os.getenv("ELASTICSEARCH_HOST", "https://127.0.0.1")
|
||||
|
||||
@@ -243,6 +243,33 @@ class QWenCV(GptV4):
|
||||
tmp_path = tmp.name
|
||||
|
||||
video_path = f"file://{tmp_path}"
|
||||
prompt_ch = """
|
||||
你是一名专业的视频转录助手,能够将视频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
|
||||
**任务要求**:
|
||||
1.输入是MP4等视频文件,解析带时间戳的文本。
|
||||
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
|
||||
3.时间戳需尽可能贴近实际视频的起止时间,误差不超过1秒。
|
||||
4.如果无法确定具体时间,请根据上下文合理估算。
|
||||
5.最后总结:这段视频的内容是什么?,并用恰当的句子总结这个视频。
|
||||
|
||||
**示例输出**:
|
||||
[00:00:00.000] 今天天气真好,
|
||||
[00:00:02.500] 我们一起去公园散步吧。
|
||||
[00:00:05.800] 公园里的花开得非常漂亮。
|
||||
这段视频的内容是关于如何在CREAMS系统中进行楼宇管理集合的编辑或删除操作。视频演示了 ..."""
|
||||
prompt_en = """
|
||||
You are a professional video transcription assistant, capable of transcribing the content of video files into text and **precisely marking the timestamp (start time-end time) corresponding to each sentence or paragraph**.
|
||||
**Task requirements**:
|
||||
1. Input is MP4 or other video files, and parse the text with timestamps.
|
||||
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
|
||||
3. The timestamp should be as close as possible to the actual start and end time of the video, with an error not exceeding 1 second.
|
||||
4. If the specific time cannot be determined, please make a reasonable estimation based on the context.
|
||||
5. Final summary: What is the content of this video? Summarize this video in an appropriate sentence.
|
||||
|
||||
**Example output**:
|
||||
[00:00:00.000] The weather is really nice today, [00:00:02.500] let's go for a walk in the park together.
|
||||
[00:00:05.800] The flowers in the park are blooming beautifully.
|
||||
The content of this video is about how to edit or delete building management collections in the CREAMS system. The video demonstrates .."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
@@ -252,7 +279,7 @@ class QWenCV(GptV4):
|
||||
"fps": 2,
|
||||
},
|
||||
{
|
||||
"text": "视频的内容是什么?,并且,请用恰当的句子总结这个视频。" if self.lang.lower() == "chinese" else "What is the content of the video? And please summarize this video in proper sentences.",
|
||||
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -60,6 +60,34 @@ class QWenSeq2txt(Base):
|
||||
from dashscope import MultiModalConversation
|
||||
|
||||
audio_path = f"file://{audio_path}"
|
||||
prompt_ch = """
|
||||
你是一名专业的音频转录助手,能够将MP3音频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
|
||||
**任务要求**:
|
||||
1.输入是MP3,解析带时间戳的文本。
|
||||
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
|
||||
3.时间戳需尽可能贴近实际语音的起止时间,误差不超过1秒。
|
||||
4.如果无法确定具体时间,请根据上下文合理估算。
|
||||
5.最后总结:这段音频在说什么?
|
||||
|
||||
**示例输出**:
|
||||
[00:00:00.000] 今天天气真好,
|
||||
[00:00:02.500] 我们一起去公园散步吧。
|
||||
[00:00:05.800] 公园里的花开得非常漂亮。
|
||||
这段音频讲述的是一个关于**“吃水不忘挖井人”**的感人故事,主 ..."""
|
||||
prompt_en = """
|
||||
You are a professional audio transcription assistant, capable of transcribing the content of MP3 audio files into text and **precisely marking the timestamps (start time - end time) corresponding to each sentence or paragraph**.
|
||||
**Task requirements**:
|
||||
1. Input is MP3, parse text with timestamps.
|
||||
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
|
||||
3. The timestamp should be as close as possible to the actual start and end time of the voice, with an error not exceeding 1 second.
|
||||
4. If a specific time cannot be determined, please make a reasonable estimation based on the context.
|
||||
5. Final summary: What is this audio talking about?
|
||||
|
||||
**Example Output**:
|
||||
[00:00:00.000] The weather is really nice today,
|
||||
[00:00:02.500] let's go for a walk in the park together.
|
||||
[00:00:05.800] The flowers in the park are blooming beautifully.
|
||||
This audio tells a touching story about **"Remembering the one who dug the well when drinking water"** .."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
@@ -68,7 +96,7 @@ class QWenSeq2txt(Base):
|
||||
"audio": audio_path
|
||||
},
|
||||
{
|
||||
"text": "这段音频在说什么?" if self.lang.lower() == "chinese" else "What is this audio saying?",
|
||||
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
|
||||
from app.core.workflow.graph_builder import GraphBuilder
|
||||
@@ -53,11 +54,11 @@ class WorkflowExecutor:
|
||||
self.edges = workflow_config.get("edges", [])
|
||||
self.execution_config = workflow_config.get("execution_config", {})
|
||||
|
||||
self.checkpoint_config = {
|
||||
"configurable": {
|
||||
self.checkpoint_config = RunnableConfig(
|
||||
configurable={
|
||||
"thread_id": uuid.uuid4(),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState:
|
||||
"""准备初始状态(注入系统变量和会话变量)
|
||||
@@ -214,13 +215,13 @@ class WorkflowExecutor:
|
||||
return {
|
||||
"status": "completed",
|
||||
"output": final_output,
|
||||
"variables": result.get("variables", {}),
|
||||
"node_outputs": node_outputs,
|
||||
"messages": result.get("messages", []),
|
||||
"conversation_id": conversation_id,
|
||||
"elapsed_time": elapsed_time,
|
||||
"token_usage": token_usage,
|
||||
"error": result.get("error"),
|
||||
"variables": result.get("variables", {}),
|
||||
}
|
||||
|
||||
def build_graph(self, stream=False) -> CompiledStateGraph:
|
||||
@@ -326,11 +327,10 @@ class WorkflowExecutor:
|
||||
}
|
||||
|
||||
# 1. 构建图
|
||||
graph = self.build_graph(True)
|
||||
graph = self.build_graph(stream=True)
|
||||
|
||||
# 2. 初始化状态(自动注入系统变量)
|
||||
initial_state = self._prepare_initial_state(input_data)
|
||||
|
||||
# 3. Execute workflow
|
||||
try:
|
||||
chunk_count = 0
|
||||
@@ -346,14 +346,16 @@ class WorkflowExecutor:
|
||||
mode, data = event
|
||||
else:
|
||||
# Unexpected format, log and skip
|
||||
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}")
|
||||
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}"
|
||||
f"- execution_id: {self.execution_id}")
|
||||
continue
|
||||
|
||||
if mode == "custom":
|
||||
# Handle custom streaming events (chunks from nodes via stream writer)
|
||||
chunk_count += 1
|
||||
event_type = data.get("type", "node_chunk") # "message" or "node_chunk"
|
||||
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}")
|
||||
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}"
|
||||
f"- execution_id: {self.execution_id}")
|
||||
yield {
|
||||
"event": event_type, # "message" or "node_chunk"
|
||||
"data": {
|
||||
@@ -380,7 +382,8 @@ class WorkflowExecutor:
|
||||
variables_sys = variables.get("sys", {})
|
||||
conversation_id = input_data.get("conversation_id")
|
||||
execution_id = variables_sys.get("execution_id")
|
||||
logger.info(f"[DEBUG] Node starts execution: {node_name}")
|
||||
logger.info(f"[NODE-START] Node starts execution: {node_name} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
yield {
|
||||
"event": "node_start",
|
||||
@@ -399,7 +402,8 @@ class WorkflowExecutor:
|
||||
variables_sys = variables.get("sys", {})
|
||||
conversation_id = input_data.get("conversation_id")
|
||||
execution_id = variables_sys.get("execution_id")
|
||||
logger.info(f"[DEBUG] Node execution completed: {node_name}")
|
||||
logger.info(f"[NODE-END] Node execution completed: {node_name} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
yield {
|
||||
"event": "node_end",
|
||||
@@ -407,13 +411,15 @@ class WorkflowExecutor:
|
||||
"node_id": node_name,
|
||||
"conversation_id": conversation_id,
|
||||
"execution_id": execution_id,
|
||||
"timestamp": data.get("timestamp")
|
||||
"timestamp": data.get("timestamp"),
|
||||
"state": result.get("node_outputs", {}).get(node_name),
|
||||
}
|
||||
}
|
||||
|
||||
elif mode == "updates":
|
||||
# Handle state updates - store final state
|
||||
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())}")
|
||||
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
# 计算耗时
|
||||
end_time = datetime.datetime.now()
|
||||
@@ -421,7 +427,7 @@ class WorkflowExecutor:
|
||||
result = graph.get_state(self.checkpoint_config).values
|
||||
logger.info(
|
||||
f"Workflow execution completed (streaming), "
|
||||
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s"
|
||||
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s, execution_id: {self.execution_id}"
|
||||
)
|
||||
|
||||
# 发送 workflow_end 事件
|
||||
@@ -449,7 +455,8 @@ class WorkflowExecutor:
|
||||
}
|
||||
}
|
||||
|
||||
def _extract_final_output(self, node_outputs: dict[str, Any]) -> str | None:
|
||||
@staticmethod
|
||||
def _extract_final_output(node_outputs: dict[str, Any]) -> str | None:
|
||||
"""从节点输出中提取最终输出
|
||||
|
||||
优先级:
|
||||
@@ -473,7 +480,8 @@ class WorkflowExecutor:
|
||||
|
||||
return None
|
||||
|
||||
def _aggregate_token_usage(self, node_outputs: dict[str, Any]) -> dict[str, int] | None:
|
||||
@staticmethod
|
||||
def _aggregate_token_usage(node_outputs: dict[str, Any]) -> dict[str, int] | None:
|
||||
"""聚合所有节点的 token 使用情况
|
||||
|
||||
Args:
|
||||
|
||||
@@ -25,7 +25,7 @@ class WorkflowState(TypedDict):
|
||||
The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc.
|
||||
"""
|
||||
# List of messages (append mode)
|
||||
messages: list[dict[str, str]]
|
||||
messages: Annotated[list[dict[str, str]], lambda x, y: y]
|
||||
|
||||
# Set of loop node IDs, used for assigning values in loop nodes
|
||||
cycle_nodes: list
|
||||
|
||||
@@ -21,6 +21,7 @@ class IterationRuntime:
|
||||
optional parallel execution, flattening of output, and loop control via
|
||||
the workflow state.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph: CompiledStateGraph,
|
||||
@@ -87,6 +88,7 @@ class IterationRuntime:
|
||||
self.result.append(output)
|
||||
if not result["looping"]:
|
||||
self.looping = False
|
||||
return result
|
||||
|
||||
def _create_iteration_tasks(self, array_obj, idx):
|
||||
"""
|
||||
@@ -124,7 +126,7 @@ class IterationRuntime:
|
||||
array_obj = VariablePool(self.state).get(input_expression)
|
||||
if not isinstance(array_obj, list):
|
||||
raise RuntimeError("Cannot iterate over a non-list variable")
|
||||
|
||||
child_state = []
|
||||
idx = 0
|
||||
if self.typed_config.parallel:
|
||||
# Execute iterations in parallel batches
|
||||
@@ -132,15 +134,14 @@ class IterationRuntime:
|
||||
tasks = self._create_iteration_tasks(array_obj, idx)
|
||||
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
|
||||
idx += self.typed_config.parallel_count
|
||||
await asyncio.gather(*tasks)
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return self.result
|
||||
child_state.extend(await asyncio.gather(*tasks))
|
||||
else:
|
||||
# Execute iterations sequentially
|
||||
while idx < len(array_obj) and self.looping:
|
||||
logger.info(f"Iteration node {self.node_id}: running")
|
||||
item = array_obj[idx]
|
||||
result = await self.graph.ainvoke(self._init_iteration_state(item, idx))
|
||||
child_state.append(result)
|
||||
output = VariablePool(result).get(self.output_value)
|
||||
if isinstance(output, list) and self.typed_config.flatten:
|
||||
self.result.extend(output)
|
||||
@@ -150,5 +151,8 @@ class IterationRuntime:
|
||||
self.looping = False
|
||||
idx += 1
|
||||
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return self.result
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return {
|
||||
"output": self.result,
|
||||
"__child_state": child_state
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ class LoopRuntime:
|
||||
variables=pool.get_all_conversation_vars(),
|
||||
node_outputs=pool.get_all_node_outputs(),
|
||||
system_vars=pool.get_all_system_vars(),
|
||||
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
|
||||
)
|
||||
if variable.input_type == ValueInputType.VARIABLE
|
||||
else TypeTransformer.transform(variable.value, variable.type)
|
||||
for variable in self.typed_config.cycle_vars
|
||||
}
|
||||
self.state["node_outputs"][self.node_id] = {
|
||||
@@ -76,7 +78,9 @@ class LoopRuntime:
|
||||
variables=pool.get_all_conversation_vars(),
|
||||
node_outputs=pool.get_all_node_outputs(),
|
||||
system_vars=pool.get_all_system_vars(),
|
||||
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
|
||||
)
|
||||
if variable.input_type == ValueInputType.VARIABLE
|
||||
else TypeTransformer.transform(variable.value, variable.type)
|
||||
for variable in self.typed_config.cycle_vars
|
||||
}
|
||||
loopstate = WorkflowState(
|
||||
@@ -171,10 +175,11 @@ class LoopRuntime:
|
||||
"""
|
||||
loopstate = self._init_loop_state()
|
||||
loop_time = self.typed_config.max_loop
|
||||
child_state = []
|
||||
while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0:
|
||||
logger.info(f"loop node {self.node_id}: running")
|
||||
await self.graph.ainvoke(loopstate)
|
||||
child_state.append(await self.graph.ainvoke(loopstate))
|
||||
loop_time -= 1
|
||||
|
||||
logger.info(f"loop node {self.node_id}: execution completed")
|
||||
return loopstate["runtime_vars"][self.node_id]
|
||||
return loopstate["runtime_vars"][self.node_id] | {"__child_state": child_state}
|
||||
|
||||
@@ -10,9 +10,8 @@ from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
|
||||
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
|
||||
from app.db import get_db_read
|
||||
from app.models import knowledge_model, knowledgeshare_model, ModelType
|
||||
from app.repositories import knowledge_repository
|
||||
from app.repositories import knowledge_repository, knowledgeshare_repository
|
||||
from app.schemas.chunk_schema import RetrieveType
|
||||
from app.services import knowledge_service, knowledgeshare_service
|
||||
from app.services.model_service import ModelConfigService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode):
|
||||
|
||||
filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share)
|
||||
|
||||
share_ids = knowledge_service.knowledge_repository.get_chunked_knowledgeids(
|
||||
share_ids = knowledge_repository.get_chunked_knowledgeids(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
@@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode):
|
||||
filters = [
|
||||
knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids)
|
||||
]
|
||||
items = knowledgeshare_service.knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
|
||||
items = knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ class LLMNodeConfig(BaseNodeConfig):
|
||||
)
|
||||
|
||||
memory: MemoryWindowSetting = Field(
|
||||
...,
|
||||
default_factory=MemoryWindowSetting,
|
||||
description="对话上下文窗口"
|
||||
)
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class LLMNode(BaseNode):
|
||||
"""
|
||||
|
||||
# 1. 处理消息格式(优先使用 messages)
|
||||
self.typed_config = LLMNodeConfig(**self.config)
|
||||
messages_config = self.typed_config.messages
|
||||
|
||||
if messages_config:
|
||||
@@ -167,7 +168,7 @@ class LLMNode(BaseNode):
|
||||
Returns:
|
||||
LLM 响应消息
|
||||
"""
|
||||
self.typed_config = LLMNodeConfig(**self.config)
|
||||
# self.typed_config = LLMNodeConfig(**self.config)
|
||||
llm, prompt_or_messages = self._prepare_llm(state, True)
|
||||
|
||||
logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)")
|
||||
@@ -269,12 +270,16 @@ class LLMNode(BaseNode):
|
||||
chunk_count = 0
|
||||
|
||||
# 调用 LLM(流式,支持字符串或消息列表)
|
||||
async for chunk in llm.astream(prompt_or_messages):
|
||||
last_meta_data = {}
|
||||
async for chunk in llm.astream(prompt_or_messages, stream_usage=True):
|
||||
# 提取内容
|
||||
if hasattr(chunk, 'content'):
|
||||
content = chunk.content
|
||||
else:
|
||||
content = str(chunk)
|
||||
if hasattr(chunk, 'response_metadata'):
|
||||
if chunk.response_metadata:
|
||||
last_meta_data = chunk.response_metadata
|
||||
|
||||
# 只有当内容不为空时才处理
|
||||
if content:
|
||||
@@ -288,13 +293,10 @@ class LLMNode(BaseNode):
|
||||
logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}")
|
||||
|
||||
# 构建完整的 AIMessage(包含元数据)
|
||||
if isinstance(last_chunk, AIMessage):
|
||||
final_message = AIMessage(
|
||||
content=full_response,
|
||||
response_metadata=last_chunk.response_metadata if hasattr(last_chunk, 'response_metadata') else {}
|
||||
)
|
||||
else:
|
||||
final_message = AIMessage(content=full_response)
|
||||
final_message = AIMessage(
|
||||
content=full_response,
|
||||
response_metadata=last_meta_data
|
||||
)
|
||||
|
||||
# yield 完成标记
|
||||
yield {"__final__": True, "result": final_message}
|
||||
|
||||
@@ -76,5 +76,5 @@ __all__ = [
|
||||
"ToolStatus",
|
||||
"AuthType",
|
||||
"ExecutionStatus",
|
||||
"MemoryPerceptualModel"
|
||||
"MemoryPerceptualModel",
|
||||
]
|
||||
|
||||
@@ -75,6 +75,14 @@ class WorkflowExecution(Base):
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
release_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("app_releases.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
app_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("apps.id", ondelete="CASCADE"),
|
||||
|
||||
@@ -81,7 +81,7 @@ class DataConfigRepository:
|
||||
n.description AS description,
|
||||
n.entity_type AS entity_type,
|
||||
n.name AS name,
|
||||
n.fact_summary AS fact_summary,
|
||||
COALESCE(n.fact_summary, '') AS fact_summary,
|
||||
n.group_id AS group_id,
|
||||
n.apply_id AS apply_id,
|
||||
n.user_id AS user_id,
|
||||
@@ -115,7 +115,7 @@ class DataConfigRepository:
|
||||
description: n.description,
|
||||
entity_type: n.entity_type,
|
||||
name: n.name,
|
||||
fact_summary: n.fact_summary,
|
||||
fact_summary: COALESCE(n.fact_summary, ''),
|
||||
id: n.id
|
||||
} AS sourceNode,
|
||||
{
|
||||
@@ -132,7 +132,7 @@ class DataConfigRepository:
|
||||
description: m.description,
|
||||
entity_type: m.entity_type,
|
||||
name: m.name,
|
||||
fact_summary: m.fact_summary,
|
||||
fact_summary: COALESCE(m.fact_summary, ''),
|
||||
id: m.id
|
||||
} AS targetNode
|
||||
"""
|
||||
|
||||
@@ -25,7 +25,7 @@ class MemoryIncrementRepository:
|
||||
MemoryIncrement,
|
||||
func.row_number().over(
|
||||
partition_by=func.date(MemoryIncrement.created_at), # 按日期分区
|
||||
order_by=MemoryIncrement.created_at.desc() # 按时间戳升序排序
|
||||
order_by=MemoryIncrement.created_at.desc() # 按时间戳降序排序,取每天最新的
|
||||
).label('row_num')
|
||||
)
|
||||
.filter(MemoryIncrement.workspace_id == workspace_id)
|
||||
@@ -34,14 +34,24 @@ class MemoryIncrementRepository:
|
||||
|
||||
memory_increment_alias = aliased(MemoryIncrement, subquery)
|
||||
|
||||
memory_increments = (
|
||||
# 先取最近的limit条记录的子查询
|
||||
recent_records_subquery = (
|
||||
self.db.query(memory_increment_alias)
|
||||
.filter(subquery.c.row_num == 1) # 只取每个日期的第一条(最新的)
|
||||
.order_by(memory_increment_alias.created_at.asc()) # 按时间戳降序排序
|
||||
.order_by(memory_increment_alias.created_at.desc()) # 按时间戳降序排序,取最近的
|
||||
.limit(limit)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# 在外层按升序排列(从旧到新)
|
||||
recent_alias = aliased(MemoryIncrement, recent_records_subquery)
|
||||
memory_increments = (
|
||||
self.db.query(recent_alias)
|
||||
.order_by(recent_alias.created_at.asc()) # 按时间戳升序排序
|
||||
.all()
|
||||
)
|
||||
db_logger.info(f"成功查询工作空间 {workspace_id} 下的内存增量")
|
||||
|
||||
db_logger.info(f"成功查询工作空间 {workspace_id} 下的内存增量,返回最近 {len(memory_increments)} 条记录")
|
||||
return memory_increments
|
||||
except Exception as e:
|
||||
db_logger.error(f"查询工作空间 {workspace_id} 下内存增量时出错: {str(e)}")
|
||||
|
||||
@@ -332,7 +332,7 @@ RETURN e.id AS id,
|
||||
e.description AS description,
|
||||
e.aliases AS aliases,
|
||||
e.name_embedding AS name_embedding,
|
||||
e.fact_summary AS fact_summary,
|
||||
COALESCE(e.fact_summary, '') AS fact_summary,
|
||||
e.connect_strength AS connect_strength,
|
||||
collect(DISTINCT s.id) AS statement_ids,
|
||||
collect(DISTINCT c.id) AS chunk_ids,
|
||||
|
||||
@@ -30,3 +30,9 @@ class EmotionSuggestionsRequest(BaseModel):
|
||||
"""获取个性化情绪建议请求"""
|
||||
group_id: str = Field(..., description="组ID")
|
||||
config_id: Optional[int] = Field(None, description="配置ID(用于指定LLM模型)")
|
||||
|
||||
|
||||
class EmotionGenerateSuggestionsRequest(BaseModel):
|
||||
"""生成个性化情绪建议请求"""
|
||||
group_id: str = Field(..., description="组ID")
|
||||
config_id: Optional[int] = Field(None, description="配置ID(用于指定LLM模型)")
|
||||
|
||||
@@ -262,3 +262,25 @@ InterestCategory = InterestCategoryResponse
|
||||
InterestAreaDistribution = InterestAreaDistributionResponse
|
||||
BehaviorHabit = BehaviorHabitResponse
|
||||
UserProfile = UserProfileResponse
|
||||
|
||||
|
||||
# Cache-related Schemas
|
||||
|
||||
class GenerateProfileRequest(BaseModel):
|
||||
"""生成完整用户画像请求"""
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
|
||||
|
||||
class CompleteProfileResponse(BaseModel):
|
||||
"""完整用户画像响应(包含所有模块)"""
|
||||
user_id: str
|
||||
preferences: List[PreferenceTagResponse]
|
||||
portrait: DimensionPortraitResponse
|
||||
interest_areas: InterestAreaDistributionResponse
|
||||
habits: List[BehaviorHabitResponse]
|
||||
generated_at: datetime.datetime
|
||||
cached: bool = Field(False, description="是否来自缓存")
|
||||
|
||||
@field_serializer("generated_at", when_used="json")
|
||||
def _serialize_generated_at(self, dt: datetime.datetime):
|
||||
return int(dt.timestamp() * 1000) if dt else None
|
||||
|
||||
@@ -527,6 +527,7 @@ class AppChatService:
|
||||
conversation_id: uuid.UUID,
|
||||
config: WorkflowConfig,
|
||||
app_id: uuid.UUID,
|
||||
release_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
@@ -549,6 +550,7 @@ class AppChatService:
|
||||
payload=payload,
|
||||
config=config,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
async def workflow_chat_stream(
|
||||
@@ -557,6 +559,7 @@ class AppChatService:
|
||||
conversation_id: uuid.UUID,
|
||||
config: WorkflowConfig,
|
||||
app_id: uuid.UUID,
|
||||
release_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
user_id: str = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
@@ -565,7 +568,7 @@ class AppChatService:
|
||||
storage_type: Optional[str] = None,
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
|
||||
) -> AsyncGenerator[str, None]:
|
||||
) -> AsyncGenerator[dict, None]:
|
||||
"""聊天(流式)"""
|
||||
workflow_service = WorkflowService(self.db)
|
||||
payload = DraftRunRequest(
|
||||
@@ -580,6 +583,7 @@ class AppChatService:
|
||||
payload=payload,
|
||||
config=config,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release_id
|
||||
):
|
||||
yield event
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class AppService:
|
||||
Raises:
|
||||
ResourceNotFoundException: 当应用不存在时
|
||||
"""
|
||||
app = get_apps_by_id(self.db,app_id)
|
||||
app = get_apps_by_id(self.db, app_id)
|
||||
if not app:
|
||||
logger.warning("应用不存在", extra={"app_id": str(app_id)})
|
||||
raise ResourceNotFoundException("应用", str(app_id))
|
||||
@@ -227,7 +227,6 @@ class AppService:
|
||||
if not model_api_key:
|
||||
raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id))
|
||||
|
||||
|
||||
# 3. 检查子 Agent 配置
|
||||
if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0:
|
||||
raise BusinessException(
|
||||
@@ -281,10 +280,10 @@ class AppService:
|
||||
)
|
||||
|
||||
def _create_agent_config(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: app_schema.AgentConfigCreate,
|
||||
now: datetime.datetime
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: app_schema.AgentConfigCreate,
|
||||
now: datetime.datetime
|
||||
) -> None:
|
||||
"""创建 Agent 配置(内部方法)
|
||||
|
||||
@@ -313,10 +312,10 @@ class AppService:
|
||||
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
|
||||
|
||||
def _create_multi_agent_config(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: Dict[str, Any],
|
||||
now: datetime.datetime
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: Dict[str, Any],
|
||||
now: datetime.datetime
|
||||
) -> None:
|
||||
"""创建多 Agent 配置(内部方法)
|
||||
|
||||
@@ -411,9 +410,9 @@ class AppService:
|
||||
return 1 if max_ver is None else int(max_ver) + 1
|
||||
|
||||
def _convert_to_schema(
|
||||
self,
|
||||
app: App,
|
||||
current_workspace_id: uuid.UUID
|
||||
self,
|
||||
app: App,
|
||||
current_workspace_id: uuid.UUID
|
||||
) -> app_schema.App:
|
||||
"""将 App 模型转换为 Schema,并设置 is_shared 字段
|
||||
|
||||
@@ -447,9 +446,9 @@ class AppService:
|
||||
# ==================== 应用管理 ====================
|
||||
|
||||
def get_app(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> App:
|
||||
"""获取应用详情
|
||||
|
||||
@@ -469,11 +468,11 @@ class AppService:
|
||||
return app
|
||||
|
||||
def create_app(
|
||||
self,
|
||||
*,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
data: app_schema.AppCreate
|
||||
self,
|
||||
*,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
data: app_schema.AppCreate
|
||||
) -> App:
|
||||
"""创建应用
|
||||
|
||||
@@ -535,11 +534,11 @@ class AppService:
|
||||
raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||||
|
||||
def update_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AppUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AppUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> App:
|
||||
"""更新应用基本信息
|
||||
|
||||
@@ -578,10 +577,10 @@ class AppService:
|
||||
return app
|
||||
|
||||
def delete_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> None:
|
||||
"""删除应用
|
||||
|
||||
@@ -612,12 +611,12 @@ class AppService:
|
||||
)
|
||||
|
||||
def copy_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
new_name: Optional[str] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
new_name: Optional[str] = None
|
||||
) -> App:
|
||||
"""复制应用(包括基础信息和配置)
|
||||
|
||||
@@ -716,16 +715,16 @@ class AppService:
|
||||
raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||||
|
||||
def list_apps(
|
||||
self,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
self,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
) -> Tuple[List[App], int]:
|
||||
"""列出工作空间中的应用(分页)
|
||||
|
||||
@@ -759,8 +758,7 @@ class AppService:
|
||||
)
|
||||
|
||||
# 构建查询条件
|
||||
filters = []
|
||||
filters.append(App.is_active == True)
|
||||
filters = [App.is_active == True]
|
||||
if type:
|
||||
filters.append(App.type == type)
|
||||
if visibility:
|
||||
@@ -813,9 +811,9 @@ class AppService:
|
||||
return items, int(total)
|
||||
|
||||
def get_apps_by_ids(
|
||||
self,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
self,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
) -> List[App]:
|
||||
"""根据ID列表获取应用
|
||||
|
||||
@@ -846,11 +844,11 @@ class AppService:
|
||||
# ==================== Agent 配置管理 ====================
|
||||
|
||||
def update_agent_config(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AgentConfig:
|
||||
"""更新 Agent 配置
|
||||
|
||||
@@ -875,7 +873,8 @@ class AppService:
|
||||
|
||||
self._validate_workspace_access(app, workspace_id)
|
||||
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active==True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
|
||||
AgentConfig.updated_at.desc())
|
||||
agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
@@ -918,10 +917,10 @@ class AppService:
|
||||
return agent_cfg
|
||||
|
||||
def get_agent_config(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AgentConfig:
|
||||
"""获取 Agent 配置
|
||||
|
||||
@@ -948,7 +947,12 @@ class AppService:
|
||||
# 只读操作,允许访问共享应用
|
||||
self._validate_app_accessible(app, workspace_id)
|
||||
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(
|
||||
AgentConfig.app_id == app_id,
|
||||
AgentConfig.is_active.is_(True)
|
||||
).order_by(
|
||||
AgentConfig.updated_at.desc()
|
||||
)
|
||||
config = self.db.scalars(stmt).first()
|
||||
|
||||
if config:
|
||||
@@ -1166,13 +1170,13 @@ class AppService:
|
||||
# ==================== 应用发布管理 ====================
|
||||
|
||||
def publish(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
publisher_id: uuid.UUID,
|
||||
version_name: str,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
release_notes: Optional[str] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
publisher_id: uuid.UUID,
|
||||
version_name: str,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
release_notes: Optional[str] = None
|
||||
) -> AppRelease:
|
||||
"""发布应用(创建不可变快照)
|
||||
|
||||
@@ -1200,7 +1204,8 @@ class AppService:
|
||||
default_model_config_id = None
|
||||
|
||||
if app.type == AppType.AGENT:
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
|
||||
AgentConfig.updated_at.desc())
|
||||
agent_cfg = self.db.scalars(stmt).first()
|
||||
if not agent_cfg:
|
||||
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
|
||||
@@ -1236,8 +1241,7 @@ class AppService:
|
||||
default_model_config_id = multi_agent_cfg.default_model_config_id
|
||||
|
||||
# 4. 构建配置快照
|
||||
|
||||
|
||||
|
||||
config = {
|
||||
"model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters),
|
||||
"master_agent_id": str(multi_agent_cfg.master_agent_id),
|
||||
@@ -1264,6 +1268,7 @@ class AppService:
|
||||
raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING)
|
||||
|
||||
config = {
|
||||
"id": str(workflow_cfg.id),
|
||||
"nodes": workflow_cfg.nodes,
|
||||
"edges": workflow_cfg.edges,
|
||||
"variables": workflow_cfg.variables,
|
||||
@@ -1285,7 +1290,7 @@ class AppService:
|
||||
id=uuid.uuid4(),
|
||||
app_id=app_id,
|
||||
version=version,
|
||||
version_name = version_name,
|
||||
version_name=version_name,
|
||||
release_notes=release_notes,
|
||||
name=app.name,
|
||||
description=app.description,
|
||||
@@ -1319,10 +1324,10 @@ class AppService:
|
||||
return release
|
||||
|
||||
def get_current_release(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> Optional[AppRelease]:
|
||||
"""获取当前发布版本
|
||||
|
||||
@@ -1349,10 +1354,10 @@ class AppService:
|
||||
return self.db.get(AppRelease, app.current_release_id)
|
||||
|
||||
def list_releases(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> List[AppRelease]:
|
||||
"""列出应用的所有发布版本(倒序)
|
||||
|
||||
@@ -1381,11 +1386,11 @@ class AppService:
|
||||
return list(self.db.scalars(stmt).all())
|
||||
|
||||
def rollback(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
version: int,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
version: int,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AppRelease:
|
||||
"""回滚到指定版本
|
||||
|
||||
@@ -1434,12 +1439,12 @@ class AppService:
|
||||
# ==================== 应用分享功能 ====================
|
||||
|
||||
def share_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_ids: List[uuid.UUID],
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_ids: List[uuid.UUID],
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AppShare:
|
||||
"""分享应用到其他工作空间
|
||||
|
||||
@@ -1457,7 +1462,6 @@ class AppService:
|
||||
BusinessException: 当应用不在指定工作空间或目标工作空间无效时
|
||||
"""
|
||||
|
||||
|
||||
logger.info(
|
||||
"分享应用",
|
||||
extra={
|
||||
@@ -1536,11 +1540,11 @@ class AppService:
|
||||
return shares
|
||||
|
||||
def unshare_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> None:
|
||||
"""取消应用分享
|
||||
|
||||
@@ -1594,10 +1598,10 @@ class AppService:
|
||||
)
|
||||
|
||||
def list_app_shares(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> List[AppShare]:
|
||||
"""列出应用的所有分享记录
|
||||
|
||||
@@ -1637,14 +1641,14 @@ class AppService:
|
||||
# ==================== 试运行功能 ====================
|
||||
|
||||
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
|
||||
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(使用当前草稿配置)
|
||||
|
||||
@@ -1736,14 +1740,14 @@ class AppService:
|
||||
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
|
||||
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(流式返回)
|
||||
|
||||
@@ -1794,30 +1798,30 @@ class AppService:
|
||||
# 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
|
||||
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
|
||||
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]:
|
||||
"""多模型对比试运行
|
||||
|
||||
@@ -1907,17 +1911,17 @@ class AppService:
|
||||
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
|
||||
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
|
||||
):
|
||||
"""多模型对比试运行(流式返回)
|
||||
|
||||
@@ -1982,15 +1986,15 @@ class AppService:
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -2009,7 +2013,8 @@ def create_app(db: Session, *, user_id: uuid.UUID, workspace_id: uuid.UUID, data
|
||||
return service.create_app(user_id=user_id, workspace_id=workspace_id, data=data)
|
||||
|
||||
|
||||
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate, workspace_id: uuid.UUID | None = None) -> App:
|
||||
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> App:
|
||||
"""更新应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
@@ -2021,12 +2026,15 @@ def delete_app(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None
|
||||
return service.delete_app(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate, workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||||
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||||
"""更新 Agent 配置(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
|
||||
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
|
||||
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
"""更新 Agent 配置(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
@@ -2040,6 +2048,7 @@ def get_agent_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID
|
||||
service = AppService(db)
|
||||
return service.get_agent_config(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
"""获取 Agent 配置(向后兼容接口)
|
||||
|
||||
@@ -2049,13 +2058,20 @@ def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UU
|
||||
return service.get_workflow_config(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,version_name:str, release_notes: Optional[str] = None) -> AppRelease:
|
||||
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,
|
||||
version_name: str, release_notes: Optional[str] = None) -> AppRelease:
|
||||
"""发布应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.publish(app_id=app_id, publisher_id=publisher_id,version_name = version_name, workspace_id=workspace_id, release_notes=release_notes)
|
||||
return service.publish(app_id=app_id, publisher_id=publisher_id, version_name=version_name,
|
||||
workspace_id=workspace_id, release_notes=release_notes)
|
||||
|
||||
|
||||
def get_current_release(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> Optional[AppRelease]:
|
||||
def get_current_release(
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID | None = None
|
||||
) -> Optional[AppRelease]:
|
||||
"""获取当前发布版本(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.get_current_release(app_id=app_id, workspace_id=workspace_id)
|
||||
@@ -2074,16 +2090,16 @@ def rollback(db: Session, *, app_id: uuid.UUID, version: int, workspace_id: uuid
|
||||
|
||||
|
||||
def list_apps(
|
||||
db: Session,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
db: Session,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
) -> Tuple[List[App], int]:
|
||||
"""列出应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
@@ -2100,9 +2116,9 @@ def list_apps(
|
||||
|
||||
|
||||
def get_apps_by_ids(
|
||||
db: Session,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
db: Session,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
) -> List[App]:
|
||||
"""根据ID列表获取应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
@@ -2112,14 +2128,14 @@ def get_apps_by_ids(
|
||||
# ==================== 向后兼容的函数接口 ====================
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -2134,30 +2150,28 @@ async def draft_run(
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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(
|
||||
|
||||
@@ -705,3 +705,75 @@ class EmotionAnalyticsService:
|
||||
health_summary=summary,
|
||||
suggestions=suggestions
|
||||
)
|
||||
|
||||
async def get_cached_suggestions(
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""从 Redis 缓存获取个性化情绪建议
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
|
||||
Returns:
|
||||
Dict: 缓存的建议数据,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}")
|
||||
|
||||
# 从 Redis 获取缓存
|
||||
cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id)
|
||||
|
||||
if cached_data is None:
|
||||
logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期")
|
||||
return None
|
||||
|
||||
logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}")
|
||||
return cached_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def save_suggestions_cache(
|
||||
self,
|
||||
end_user_id: str,
|
||||
suggestions_data: Dict[str, Any],
|
||||
db: Session,
|
||||
expires_hours: int = 24
|
||||
) -> None:
|
||||
"""保存建议到 Redis 缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
suggestions_data: 建议数据
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
expires_hours: 过期时间(小时),默认24小时
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
|
||||
|
||||
# 计算过期时间(秒)
|
||||
expire_seconds = expires_hours * 3600
|
||||
|
||||
# 保存到 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}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True)
|
||||
# 不抛出异常,缓存失败不应影响主流程
|
||||
@@ -11,6 +11,22 @@ from app.repositories.home_page_repository import HomePageRepository
|
||||
from app.schemas.home_page_schema import HomeStatistics, WorkspaceInfo
|
||||
|
||||
class HomePageService:
|
||||
|
||||
DEFAULT_RETURN_DATA: Dict[str, Any] = {
|
||||
"message": "",
|
||||
"introduction": {
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
},
|
||||
"introduction_en": {
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_home_statistics(db: Session, tenant_id: UUID) -> HomeStatistics:
|
||||
@@ -82,60 +98,36 @@ class HomePageService:
|
||||
:param version: 系统版本号(如 "0.2.0")
|
||||
:return: 对应版本的详细介绍
|
||||
"""
|
||||
# 1. 定义 JSON 文件路径(使用 Path 处理跨平台路径问题)
|
||||
json_file_path = Path(__file__).parent.parent / "version_info.json"
|
||||
# 转换为绝对路径,便于调试
|
||||
json_abs_path = json_file_path.resolve()
|
||||
# 2. 定义 JSON 文件路径(简化路径处理,保留绝对路径调试特性)
|
||||
json_abs_path = Path(__file__).parent.parent / "version_info.json"
|
||||
json_abs_path = json_abs_path.resolve()
|
||||
|
||||
# 3. 初始化返回结果(深拷贝默认模板,避免修改原常量)
|
||||
from copy import deepcopy
|
||||
result = deepcopy(HomePageService.DEFAULT_RETURN_DATA)
|
||||
|
||||
try:
|
||||
# 2. 读取 JSON 文件
|
||||
# 4. 简化文件存在性判断(合并逻辑,减少分支)
|
||||
if not json_abs_path.exists():
|
||||
return {
|
||||
"message": f"版本介绍文件不存在:{json_abs_path}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
result["message"] = f"版本介绍文件不存在:{json_abs_path}"
|
||||
return result
|
||||
|
||||
# 5. 读取并解析 JSON 文件(简化文件操作流程)
|
||||
with open(json_abs_path, "r", encoding="utf-8") as f:
|
||||
changelogs = json.load(f)
|
||||
|
||||
# 3. 匹配对应版本的介绍,若版本不存在返回默认提示
|
||||
if version not in changelogs:
|
||||
return {
|
||||
"message": f"暂未查询到 {version} 版本的详细介绍",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
return changelogs[version]
|
||||
# 6. 简化版本匹配逻辑,直接返回结果或更新提示信息
|
||||
if version in changelogs:
|
||||
return changelogs[version]
|
||||
result["message"] = f"暂未查询到 {version} 版本的详细介绍"
|
||||
return result
|
||||
|
||||
except FileNotFoundError as e:
|
||||
# 处理文件不存在异常
|
||||
return {
|
||||
"message": f"系统内部错误:{str(e)}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
result["message"] = f"系统内部错误:{str(e)}"
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
# 处理 JSON 格式错误
|
||||
return {
|
||||
"message": "版本介绍文件格式错误,无法解析 JSON",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
result["message"] = "版本介绍文件格式错误,无法解析 JSON"
|
||||
return result
|
||||
except Exception as e:
|
||||
# 处理其他未知异常
|
||||
return {
|
||||
"message": f"加载版本介绍失败:{str(e)}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
result["message"] = f"加载版本介绍失败:{str(e)}"
|
||||
return result
|
||||
@@ -7,6 +7,7 @@ user profiles from memory summaries.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -372,4 +373,114 @@ class ImplicitMemoryService:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get behavior habits for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
async def generate_complete_profile(
|
||||
self,
|
||||
user_id: str
|
||||
) -> dict:
|
||||
"""生成完整的用户画像(包含所有4个模块)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Dict: 包含所有模块的完整画像数据
|
||||
"""
|
||||
logger.info(f"生成完整用户画像: user={user_id}")
|
||||
|
||||
try:
|
||||
# 并行调用4个分析方法
|
||||
preferences, portrait, interest_areas, habits = await asyncio.gather(
|
||||
self.get_preference_tags(user_id=user_id),
|
||||
self.get_dimension_portrait(user_id=user_id),
|
||||
self.get_interest_area_distribution(user_id=user_id),
|
||||
self.get_behavior_habits(user_id=user_id)
|
||||
)
|
||||
|
||||
# 转换为可序列化的格式
|
||||
profile_data = {
|
||||
"preferences": [tag.model_dump(mode='json') for tag in preferences],
|
||||
"portrait": portrait.model_dump(mode='json'),
|
||||
"interest_areas": interest_areas.model_dump(mode='json'),
|
||||
"habits": [habit.model_dump(mode='json') for habit in habits]
|
||||
}
|
||||
|
||||
logger.info(f"完整用户画像生成完成: user={user_id}")
|
||||
return profile_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成完整用户画像失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_cached_profile(
|
||||
self,
|
||||
end_user_id: str,
|
||||
db: Session
|
||||
) -> Optional[dict]:
|
||||
"""从 Redis 缓存获取完整用户画像
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
|
||||
Returns:
|
||||
Dict: 缓存的画像数据,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.implicit_memory import ImplicitMemoryCache
|
||||
|
||||
logger.info(f"尝试从 Redis 缓存获取用户画像: user={end_user_id}")
|
||||
|
||||
# 从 Redis 获取缓存
|
||||
cached_data = await ImplicitMemoryCache.get_user_profile(end_user_id)
|
||||
|
||||
if cached_data is None:
|
||||
logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期")
|
||||
return None
|
||||
|
||||
logger.info(f"成功从 Redis 缓存获取用户画像: user={end_user_id}")
|
||||
return cached_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从 Redis 缓存获取用户画像失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def save_profile_cache(
|
||||
self,
|
||||
end_user_id: str,
|
||||
profile_data: dict,
|
||||
db: Session,
|
||||
expires_hours: int = 168 # 默认7天
|
||||
) -> None:
|
||||
"""保存用户画像到 Redis 缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
profile_data: 画像数据
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
expires_hours: 过期时间(小时),默认168小时(7天)
|
||||
"""
|
||||
try:
|
||||
from app.cache.memory.implicit_memory import ImplicitMemoryCache
|
||||
|
||||
logger.info(f"保存用户画像到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
|
||||
|
||||
# 计算过期时间(秒)
|
||||
expire_seconds = expires_hours * 3600
|
||||
|
||||
# 保存到 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}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True)
|
||||
# 不抛出异常,缓存失败不应影响主流程
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Annotated, AsyncGenerator
|
||||
from typing import Any, Annotated, AsyncGenerator, Optional
|
||||
|
||||
from deprecated import deprecated
|
||||
from fastapi import Depends
|
||||
@@ -14,15 +14,14 @@ from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.workflow.validator import validate_workflow_config
|
||||
from app.db import get_db
|
||||
from app.models.conversation_model import Message
|
||||
from app.models.workflow_model import WorkflowConfig, WorkflowExecution
|
||||
from app.repositories.conversation_repository import MessageRepository
|
||||
from app.repositories.workflow_repository import (
|
||||
WorkflowConfigRepository,
|
||||
WorkflowExecutionRepository,
|
||||
WorkflowNodeExecutionRepository
|
||||
)
|
||||
from app.schemas import DraftRunRequest
|
||||
from app.services.conversation_service import ConversationService
|
||||
from app.services.multi_agent_service import convert_uuids_to_str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,7 +35,7 @@ class WorkflowService:
|
||||
self.config_repo = WorkflowConfigRepository(db)
|
||||
self.execution_repo = WorkflowExecutionRepository(db)
|
||||
self.node_execution_repo = WorkflowNodeExecutionRepository(db)
|
||||
self.message_repo = MessageRepository(db)
|
||||
self.conversation_service = ConversationService(db)
|
||||
|
||||
# ==================== 配置管理 ====================
|
||||
|
||||
@@ -266,6 +265,7 @@ class WorkflowService:
|
||||
workflow_config_id: uuid.UUID,
|
||||
app_id: uuid.UUID,
|
||||
trigger_type: str,
|
||||
release_id: uuid.UUID | None = None,
|
||||
triggered_by: uuid.UUID | None = None,
|
||||
conversation_id: uuid.UUID | None = None,
|
||||
input_data: dict[str, Any] | None = None
|
||||
@@ -273,6 +273,7 @@ class WorkflowService:
|
||||
"""创建工作流执行记录
|
||||
|
||||
Args:
|
||||
release_id: 应用发布 ID
|
||||
workflow_config_id: 工作流配置 ID
|
||||
app_id: 应用 ID
|
||||
trigger_type: 触发类型
|
||||
@@ -289,6 +290,7 @@ class WorkflowService:
|
||||
execution = WorkflowExecution(
|
||||
workflow_config_id=workflow_config_id,
|
||||
app_id=app_id,
|
||||
release_id=release_id,
|
||||
conversation_id=conversation_id,
|
||||
execution_id=execution_id,
|
||||
trigger_type=trigger_type,
|
||||
@@ -337,6 +339,7 @@ class WorkflowService:
|
||||
self,
|
||||
execution_id: str,
|
||||
status: str,
|
||||
token_usage: int | None = None,
|
||||
output_data: dict[str, Any] | None = None,
|
||||
error_message: str | None = None,
|
||||
error_node_id: str | None = None
|
||||
@@ -346,6 +349,7 @@ class WorkflowService:
|
||||
Args:
|
||||
execution_id: 执行 ID
|
||||
status: 状态
|
||||
token_usage: token消耗
|
||||
output_data: 输出数据
|
||||
error_message: 错误信息
|
||||
error_node_id: 出错节点 ID
|
||||
@@ -364,6 +368,8 @@ class WorkflowService:
|
||||
)
|
||||
|
||||
execution.status = status
|
||||
if token_usage is not None:
|
||||
execution.token_usage = token_usage
|
||||
if output_data is not None:
|
||||
execution.output_data = convert_uuids_to_str(output_data)
|
||||
if error_message is not None:
|
||||
@@ -414,12 +420,14 @@ class WorkflowService:
|
||||
payload: DraftRunRequest,
|
||||
config: WorkflowConfig,
|
||||
workspace_id: uuid.UUID,
|
||||
release_id: uuid.UUID | None = None,
|
||||
):
|
||||
"""运行工作流
|
||||
|
||||
Args:
|
||||
workspace_id:
|
||||
config:
|
||||
release_id: 发布 ID
|
||||
workspace_id:工作空间 ID
|
||||
config: 配置
|
||||
payload:
|
||||
app_id: 应用 ID
|
||||
|
||||
@@ -463,7 +471,8 @@ class WorkflowService:
|
||||
trigger_type="manual",
|
||||
triggered_by=None,
|
||||
conversation_id=conversation_id_uuid,
|
||||
input_data=input_data
|
||||
input_data=input_data,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
# 3. 构建工作流配置字典
|
||||
@@ -507,20 +516,20 @@ class WorkflowService:
|
||||
|
||||
# 更新执行结果
|
||||
if result.get("status") == "completed":
|
||||
token_usage = result.get("token_usage", {}) or {}
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=result
|
||||
output_data=result,
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
final_messages = result.get("messages", [])[init_message_length:]
|
||||
for message in final_messages:
|
||||
message_obj = Message(
|
||||
self.conversation_service.add_message(
|
||||
conversation_id=conversation_id_uuid,
|
||||
role=message["role"],
|
||||
content=message["content"],
|
||||
content=message["content"]
|
||||
)
|
||||
self.message_repo.add_message(message_obj)
|
||||
self.db.commit()
|
||||
logger.info(f"Workflow Run Success, "
|
||||
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
|
||||
else:
|
||||
@@ -562,10 +571,12 @@ class WorkflowService:
|
||||
payload: DraftRunRequest,
|
||||
config: WorkflowConfig,
|
||||
workspace_id: uuid.UUID,
|
||||
release_id: Optional[uuid.UUID] = None,
|
||||
):
|
||||
"""运行工作流(流式)
|
||||
|
||||
Args:
|
||||
release_id: 发布id
|
||||
workspace_id:
|
||||
app_id: 应用 ID
|
||||
payload: 请求对象(包含 message, variables, conversation_id 等)
|
||||
@@ -611,7 +622,8 @@ class WorkflowService:
|
||||
trigger_type="manual",
|
||||
triggered_by=None,
|
||||
conversation_id=conversation_id_uuid,
|
||||
input_data=input_data
|
||||
input_data=input_data,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
# 3. 构建工作流配置字典
|
||||
@@ -653,21 +665,21 @@ class WorkflowService:
|
||||
if event.get("event") == "workflow_end":
|
||||
|
||||
status = event.get("data", {}).get("status")
|
||||
token_usage = event.get("data", {}).get("token_usage", {}) or {}
|
||||
if status == "completed":
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=event.get("data")
|
||||
output_data=event.get("data"),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
final_messages = event.get("data", {}).get("messages", [])[init_message_length:]
|
||||
for message in final_messages:
|
||||
message_obj = Message(
|
||||
self.conversation_service.add_message(
|
||||
conversation_id=conversation_id_uuid,
|
||||
role=message["role"],
|
||||
content=message["content"],
|
||||
content=message["content"]
|
||||
)
|
||||
self.message_repo.add_message(message_obj)
|
||||
self.db.commit()
|
||||
logger.info(f"Workflow Run Success, "
|
||||
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
|
||||
elif status == "failed":
|
||||
@@ -784,10 +796,12 @@ class WorkflowService:
|
||||
|
||||
# 更新执行结果
|
||||
if result.get("status") == "completed":
|
||||
token_usage = result.get("data").get("token_usage", {}) or {}
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=result.get("node_outputs", {})
|
||||
output_data=result.get("node_outputs", {}),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
else:
|
||||
self.update_execution_status(
|
||||
@@ -882,13 +896,14 @@ class WorkflowService:
|
||||
):
|
||||
# 直接转发事件(executor 已经返回正确格式)
|
||||
if event.get("event") == "workflow_end":
|
||||
|
||||
token_usage = event.get("data").get("token_usage", {}) or {}
|
||||
status = event.get("data", {}).get("status")
|
||||
if status == "completed":
|
||||
self.update_execution_status(
|
||||
execution_id,
|
||||
"completed",
|
||||
output_data=event.get("data")
|
||||
output_data=event.get("data"),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
elif status == "failed":
|
||||
self.update_execution_status(
|
||||
|
||||
@@ -53,7 +53,7 @@ nodes:
|
||||
type: end
|
||||
name: 结束
|
||||
config:
|
||||
output: "{{ llm_qa.output }}"
|
||||
output: "{{llm_qa.output}}"
|
||||
position:
|
||||
x: 900
|
||||
y: 100
|
||||
|
||||
@@ -120,12 +120,9 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig:
|
||||
|
||||
def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig:
|
||||
config_dict = release.config
|
||||
with get_db_read() as db:
|
||||
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
|
||||
source_config_id = source_config.id
|
||||
|
||||
config = WorkflowConfig(
|
||||
id=source_config_id,
|
||||
id=config_dict.get("id"),
|
||||
app_id=release.app_id,
|
||||
nodes=config_dict.get("nodes", []),
|
||||
edges=config_dict.get("edges", []),
|
||||
|
||||
@@ -1,33 +1,68 @@
|
||||
{
|
||||
"v0.2.0": {
|
||||
"codeName": "启知",
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "本次为架构升级,核心目标是把“被动存储”升级为“主动认知”,让系统具备情绪感知、情景理解与类人记忆机制,为后续多智能体协作与专业场景落地奠定底座。",
|
||||
"coreUpgrades": [
|
||||
"记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环",
|
||||
"可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。",
|
||||
"多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。",
|
||||
"Agent集群内置“记忆-知识-工具-审核”四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。"
|
||||
]
|
||||
"introduction": {
|
||||
"codeName": "启知",
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "本次为架构升级,核心目标是把\"被动存储\"升级为\"主动认知\",让系统具备情绪感知、情景理解与类人记忆机制,为后续多智能体协作与专业场景落地奠定底座。",
|
||||
"coreUpgrades": [
|
||||
"记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环",
|
||||
"可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。",
|
||||
"多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。",
|
||||
"Agent集群内置\"记忆-知识-工具-审核\"四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。"
|
||||
]
|
||||
},
|
||||
"introduction_en": {
|
||||
"codeName": "Qizhi",
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "This release marks a foundational upgrade to the system’s cognitive architecture. The core objective is to evolve the platform from passive information storage into active cognitive intelligence—enabling emotional awareness, situational understanding, and human-like memory mechanisms. This upgrade lays the groundwork for future multi-agent collaboration and domain-specific, production-grade AI applications.",
|
||||
"coreUpgrades": [
|
||||
"Human-Like Memory Architecture: A comprehensive, human-inspired memory system is introduced, encompassing emotional processing, situational memory, short-term and working memory, perceptual memory, as well as explicit and implicit memory. Combined with brain-inspired forgetting mechanisms, the system now supports a complete cognitive loop—from perception → emotion → context → long-term consolidation, closely mirroring human memory formation.",
|
||||
"Visual Workflow Orchestration: A fully visual, drag-and-drop workflow enables modular composition of LLMs, knowledge bases, logic, and tools. This dramatically reduces the time required to move from experimentation to production—from days to hours.",
|
||||
"Multimodal Knowledge Processing: The system now supports one-click parsing and ingestion of PDF, PPT, MP3, and MP4 content. With time-aware retrieval accuracy reaching 94.3%, structured Q&A data becomes instantly usable for downstream reasoning and generation.",
|
||||
"Built-in Agent Clusters: Predefined role templates across four categories—Memory, Knowledge, Tools, and Review—can be generated with a single click. A Coordinator Agent decomposes complex tasks into parallel subtasks, while situational memory is used to resolve conflicts, validate consistency, and synthesize outputs into a coherent, end-to-end report."
|
||||
]
|
||||
}
|
||||
},
|
||||
"v0.1.0": {
|
||||
"codeName": "初心",
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "这是一款专注于管理和利用AI记忆的工具,支持RAG和知识图谱两种主流存储方式,旨在为AI应用提供持久化、结构化的“记忆”能力。",
|
||||
"coreUpgrades": [
|
||||
"记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。",
|
||||
"记忆配置:简化了配置流程,内置自动提取关键信息的“记忆萃取”和管理生命周期的\"遗忘\"引擎。",
|
||||
"知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。",
|
||||
"全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。",
|
||||
"测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。",
|
||||
"记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。",
|
||||
"集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。",
|
||||
"界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。",
|
||||
"起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。",
|
||||
"版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。",
|
||||
"文档资源:用户手册、API文档、FAQ",
|
||||
"问题反馈:GitHub Issues、邮件支持",
|
||||
"致谢:感谢所有参与测试和提供反馈的用户!"
|
||||
]
|
||||
"introduction": {
|
||||
"codeName": "初心",
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "这是一款专注于管理和利用AI记忆的工具,支持RAG和知识图谱两种主流存储方式,旨在为AI应用提供持久化、结构化的\"记忆\"能力。",
|
||||
"coreUpgrades": [
|
||||
"记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。",
|
||||
"记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。",
|
||||
"知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。",
|
||||
"全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。",
|
||||
"测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。",
|
||||
"记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。",
|
||||
"集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。",
|
||||
"界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。",
|
||||
"起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。",
|
||||
"版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。",
|
||||
"文档资源:用户手册、API文档、FAQ",
|
||||
"问题反馈:GitHub Issues、邮件支持",
|
||||
"致谢:感谢所有参与测试和提供反馈的用户!"
|
||||
]
|
||||
},
|
||||
"introduction_en": {
|
||||
"codeName": "Original Intent",
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "A tool focused on managing and utilizing AI memory, supporting both RAG and knowledge graph storage methods, aiming to provide persistent and structured 'memory' capabilities for AI applications.",
|
||||
"coreUpgrades": [
|
||||
"Memory Space: Users can create independent spaces to isolate different memories and flexibly choose storage methods.",
|
||||
"Memory Configuration: Simplified configuration process with built-in 'memory extraction' for automatic key information extraction and 'forgetting' engine for lifecycle management.",
|
||||
"Knowledge Retrieval: Provides semantic, tokenization, and hybrid retrieval modes with various parameter tuning and result reranking to improve recall.",
|
||||
"Global Management: Supports unified default retrieval parameter settings with one-click application to all knowledge bases.",
|
||||
"Testing & Debugging: Built-in 'recall testing' for real-time verification of retrieval effects and parameter adjustment, with sharing code support for collaboration.",
|
||||
"Memory Insights: View detailed conversation records, user profiles, and analysis reports to understand AI 'memory' content.",
|
||||
"Integration & Management: Provides API Key for system integration with basic user management features.",
|
||||
"Interface & Experience: Modern card-based layout with gradient design, focusing on interaction fluidity and visual aesthetics.",
|
||||
"Getting Started: Documentation provides clear basic usage flow, guiding users from creating spaces, configuring memory to testing retrieval.",
|
||||
"Version Notes: MemoryBear v0.1.0 'Original Intent' encompasses core concepts and basic capabilities of intelligent memory management, laying foundation for future development.",
|
||||
"Documentation: User Manual, API Documentation, FAQ",
|
||||
"Feedback: GitHub Issues, Email Support",
|
||||
"Acknowledgments: Thanks to all users who participated in testing and provided feedback!"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
62
api/migrations/versions/1fd7d0e703b3_20260116.py
Normal file
62
api/migrations/versions/1fd7d0e703b3_20260116.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""20260116
|
||||
|
||||
Revision ID: 1fd7d0e703b3
|
||||
Revises: 9ab9b6393f32
|
||||
Create Date: 2026-01-16 13:17:37.060026
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1fd7d0e703b3'
|
||||
down_revision: Union[str, None] = '9ab9b6393f32'
|
||||
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('emotion_suggestions_cache',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('end_user_id', sa.String(length=255), nullable=False, comment='终端用户ID(组ID)'),
|
||||
sa.Column('health_summary', sa.Text(), nullable=False, comment='健康状态摘要'),
|
||||
sa.Column('suggestions', sa.JSON(), nullable=False, comment='建议列表(JSON格式)'),
|
||||
sa.Column('generated_at', sa.DateTime(), nullable=False, comment='生成时间'),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=True, comment='过期时间'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_emotion_suggestions_cache_end_user_id'), 'emotion_suggestions_cache', ['end_user_id'], unique=True)
|
||||
op.create_index(op.f('ix_emotion_suggestions_cache_id'), 'emotion_suggestions_cache', ['id'], unique=False)
|
||||
op.create_table('implicit_memory_cache',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('end_user_id', sa.String(length=255), nullable=False, comment='终端用户ID'),
|
||||
sa.Column('preferences', sa.JSON(), nullable=False, comment='偏好标签列表(JSON格式)'),
|
||||
sa.Column('portrait', sa.JSON(), nullable=False, comment='四维画像对象(JSON格式)'),
|
||||
sa.Column('interest_areas', sa.JSON(), nullable=False, comment='兴趣领域分布对象(JSON格式)'),
|
||||
sa.Column('habits', sa.JSON(), nullable=False, comment='行为习惯列表(JSON格式)'),
|
||||
sa.Column('generated_at', sa.DateTime(), nullable=False, comment='生成时间'),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=True, comment='过期时间'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_implicit_memory_cache_end_user_id'), 'implicit_memory_cache', ['end_user_id'], unique=True)
|
||||
op.create_index(op.f('ix_implicit_memory_cache_id'), 'implicit_memory_cache', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_implicit_memory_cache_id'), table_name='implicit_memory_cache')
|
||||
op.drop_index(op.f('ix_implicit_memory_cache_end_user_id'), table_name='implicit_memory_cache')
|
||||
op.drop_table('implicit_memory_cache')
|
||||
op.drop_index(op.f('ix_emotion_suggestions_cache_id'), table_name='emotion_suggestions_cache')
|
||||
op.drop_index(op.f('ix_emotion_suggestions_cache_end_user_id'), table_name='emotion_suggestions_cache')
|
||||
op.drop_table('emotion_suggestions_cache')
|
||||
# ### end Alembic commands ###
|
||||
34
api/migrations/versions/8cd790908f92_202601191615.py
Normal file
34
api/migrations/versions/8cd790908f92_202601191615.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""202601191615
|
||||
|
||||
Revision ID: 8cd790908f92
|
||||
Revises: 1fd7d0e703b3
|
||||
Create Date: 2026-01-19 16:15:35.058649
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '8cd790908f92'
|
||||
down_revision: Union[str, None] = '1fd7d0e703b3'
|
||||
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.add_column('workflow_executions', sa.Column('release_id', sa.UUID(), nullable=True))
|
||||
op.create_index(op.f('ix_workflow_executions_release_id'), 'workflow_executions', ['release_id'], unique=False)
|
||||
op.create_foreign_key(None, 'workflow_executions', 'app_releases', ['release_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'workflow_executions', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_workflow_executions_release_id'), table_name='workflow_executions')
|
||||
op.drop_column('workflow_executions', 'release_id')
|
||||
# ### end Alembic commands ###
|
||||
@@ -31,6 +31,12 @@ export interface versionResponse{
|
||||
coreUpgrades: string[];
|
||||
codeName: string;
|
||||
};
|
||||
introduction_en?: {
|
||||
releaseDate: string;
|
||||
upgradePosition: string;
|
||||
coreUpgrades: string[];
|
||||
codeName: string;
|
||||
};
|
||||
}
|
||||
// 首页数据统计
|
||||
export const getDashboardData = `/home-page/workspaces`
|
||||
|
||||
21
web/src/assets/images/workflow/deleteBg.svg
Normal file
21
web/src/assets/images/workflow/deleteBg.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--流程控制-条件分支" transform="translate(-1752, -352)" stroke="#5B6167">
|
||||
<g id="编组-37" transform="translate(1480, 63)">
|
||||
<g id="编组-35" transform="translate(8, 195)">
|
||||
<g id="编组-33" transform="translate(264, 94)">
|
||||
<g id="编组-32" transform="translate(3, 3.5)">
|
||||
<line x1="-1.63757896e-14" y1="2" x2="10" y2="2" id="路径-29"></line>
|
||||
<polyline id="路径-30" stroke-linejoin="round" points="3 1.99990611 3 0 7 0 7 2"></polyline>
|
||||
<path d="M1.5,2.01228712 L1.5,8 C1.5,8.55228475 1.94771525,9 2.5,9 L7.5,9 C8.05228475,9 8.5,8.55228475 8.5,8 L8.5,2 L8.5,2" id="路径-31" stroke-linejoin="round"></path>
|
||||
<line x1="4" y1="4.00683364" x2="4" y2="7.00683364" id="路径-32"></line>
|
||||
<line x1="6" y1="4.00683364" x2="6" y2="7.00683364" id="路径-32"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
22
web/src/assets/images/workflow/deleteBg_hover.svg
Normal file
22
web/src/assets/images/workflow/deleteBg_hover.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--流程控制-条件分支" transform="translate(-1752, -420)">
|
||||
<g id="编组-37" transform="translate(1480, 63)">
|
||||
<g id="编组-35" transform="translate(8, 195)">
|
||||
<g id="编组-33" transform="translate(264, 162)">
|
||||
<rect id="矩形" fill="#FF5D34" opacity="0.116987" x="0" y="0" width="16" height="16" rx="4"></rect>
|
||||
<g id="编组-32" transform="translate(3, 3.5)" stroke="#FF5D34">
|
||||
<line x1="-1.63757896e-14" y1="2" x2="10" y2="2" id="路径-29"></line>
|
||||
<polyline id="路径-30" stroke-linejoin="round" points="3 1.99990611 3 0 7 0 7 2"></polyline>
|
||||
<path d="M1.5,2.01228712 L1.5,8 C1.5,8.55228475 1.94771525,9 2.5,9 L7.5,9 C8.05228475,9 8.5,8.55228475 8.5,8 L8.5,2 L8.5,2" id="路径-31" stroke-linejoin="round"></path>
|
||||
<line x1="4" y1="4.00683364" x2="4" y2="7.00683364" id="路径-32"></line>
|
||||
<line x1="6" y1="4.00683364" x2="6" y2="7.00683364" id="路径-32"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
web/src/assets/images/workflow/delete_cycle.svg
Normal file
18
web/src/assets/images/workflow/delete_cycle.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-大语言模型" transform="translate(-1736, -662)" stroke="#212332" stroke-width="1.1">
|
||||
<g id="编组-34" transform="translate(1472, 64)">
|
||||
<g id="编组-3备份-10" transform="translate(12, 409)">
|
||||
<g id="选择备份" transform="translate(0, 177)">
|
||||
<g id="编组-33" transform="translate(252, 12)">
|
||||
<circle id="椭圆形" cx="8" cy="8" r="6.45"></circle>
|
||||
<line x1="6" y1="8" x2="10" y2="8" id="路径-10" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
web/src/assets/images/workflow/recall.svg
Normal file
18
web/src/assets/images/workflow/recall.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>召回</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-知识检索" transform="translate(-1684, -330)" stroke="#212332">
|
||||
<g id="节点属性" transform="translate(1472, 64)">
|
||||
<g id="编组-30" transform="translate(12, 264)">
|
||||
<g id="编组-28" transform="translate(196, 0)">
|
||||
<g id="召回" transform="translate(4, 2)">
|
||||
<path d="M3.00336574,8.78243131 C3.31276784,9.47513403 3.79697125,10.0726443 4.39990015,10.5188863 M5.34710411,11.051994 C5.85707009,11.2602342 6.41513255,11.375 7,11.375 C7.46865477,11.375 7.92009851,11.3013108 8.34337671,11.1648869 M9.11238544,10.8321699 C9.85595277,10.4214229 10.4672402,9.80055221 10.8661626,9.04964308 M11.1846924,8.28028469 C11.3084287,7.87534253 11.375,7.44544554 11.375,7 C11.375,4.58375422 9.41624578,2.625 7,2.625 C5.30981329,2.625 3.84348335,3.58344477 3.11486142,4.98648308" id="形状"></path>
|
||||
<polyline id="路径-10" stroke-linejoin="round" points="2.48490579 2.81937431 2.86401413 5.38855621 5.4725833 4.82767632"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
web/src/assets/images/workflow/recall_hover.svg
Normal file
18
web/src/assets/images/workflow/recall_hover.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>召回</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-知识检索" transform="translate(-1684, -330)" stroke="#155EEF">
|
||||
<g id="节点属性" transform="translate(1472, 64)">
|
||||
<g id="编组-30" transform="translate(12, 264)">
|
||||
<g id="编组-28" transform="translate(196, 0)">
|
||||
<g id="召回" transform="translate(4, 2)">
|
||||
<path d="M3.00336574,8.78243131 C3.31276784,9.47513403 3.79697125,10.0726443 4.39990015,10.5188863 M5.34710411,11.051994 C5.85707009,11.2602342 6.41513255,11.375 7,11.375 C7.46865477,11.375 7.92009851,11.3013108 8.34337671,11.1648869 M9.11238544,10.8321699 C9.85595277,10.4214229 10.4672402,9.80055221 10.8661626,9.04964308 M11.1846924,8.28028469 C11.3084287,7.87534253 11.375,7.44544554 11.375,7 C11.375,4.58375422 9.41624578,2.625 7,2.625 C5.30981329,2.625 3.84348335,3.58344477 3.11486142,4.98648308" id="形状"></path>
|
||||
<polyline id="路径-10" stroke-linejoin="round" points="2.48490579 2.81937431 2.86401413 5.38855621 5.4725833 4.82767632"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
12
web/src/components/FormItem/DescWrapper.tsx
Normal file
12
web/src/components/FormItem/DescWrapper.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
const DescWrapper: FC<{desc: string | ReactNode, className?: string}> = ({desc, className}) => {
|
||||
return (
|
||||
<div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
|
||||
{desc}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DescWrapper
|
||||
13
web/src/components/FormItem/LabelWrapper.tsx
Normal file
13
web/src/components/FormItem/LabelWrapper.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
const LabelWrapper: FC<{ title: string | ReactNode, className?: string; children?: ReactNode}> = ({title, className, children}) => {
|
||||
return (
|
||||
<div className={clsx(className)}>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5">{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelWrapper
|
||||
45
web/src/components/FormItem/SwitchFormItem.tsx
Normal file
45
web/src/components/FormItem/SwitchFormItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Switch, Form, ConfigProvider } from "antd";
|
||||
import useSize from 'antd/lib/config-provider/hooks/useSize'
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
import LabelWrapper from './LabelWrapper'
|
||||
import DescWrapper from './DescWrapper'
|
||||
|
||||
interface SwitchFormItemProps {
|
||||
title: string | ReactNode;
|
||||
desc?: string | ReactNode;
|
||||
name: string | string[];
|
||||
size?: 'small' | 'default'
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SwitchFormItem: FC<SwitchFormItemProps> = ({
|
||||
title,
|
||||
desc,
|
||||
name,
|
||||
size = 'default',
|
||||
className,
|
||||
disabled
|
||||
}) => {
|
||||
const componentSize = useSize()
|
||||
console.log('componentSize', componentSize)
|
||||
|
||||
return (
|
||||
<div className={`${className} rb:flex rb:items-center rb:justify-between`}>
|
||||
<LabelWrapper title={title}>
|
||||
{desc && <DescWrapper desc={desc} className="rb:mt-2" />}
|
||||
</LabelWrapper>
|
||||
<Form.Item
|
||||
name={name}
|
||||
valuePropName="checked"
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<Switch disabled={disabled} size={size} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwitchFormItem
|
||||
@@ -54,7 +54,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
key: '1',
|
||||
label: (<>
|
||||
<div>{user.username}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]">{user.email}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">{user.email}</div>
|
||||
</>),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ const { Content } = Layout;
|
||||
|
||||
// 认证布局组件,使用useRouteGuard hook进行路由鉴权
|
||||
const AuthLayout: FC = () => {
|
||||
const { getUserInfo, getStorageType } = useUser();
|
||||
const { getUserInfo } = useUser();
|
||||
// 使用路由守卫hook处理认证和权限检查
|
||||
useRouteGuard('manage');
|
||||
// 自动更新面包屑导航
|
||||
@@ -24,7 +24,6 @@ const AuthLayout: FC = () => {
|
||||
window.location.href = `/#/login`;
|
||||
} else {
|
||||
getUserInfo()
|
||||
getStorageType()
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -150,9 +150,19 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
// 处理键盘快捷键
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.toString()) {
|
||||
navigator.clipboard.writeText(selection.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览模式
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
<div className="rb:relative" onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<style>{`
|
||||
.html-comment {
|
||||
color: #999;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.rb-modal .ant-modal-header {
|
||||
margin-bottom: 24px;
|
||||
.rb-modal .ant-modal-footer .ant-btn {
|
||||
height: 32px !important;
|
||||
padding: 0 15px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import { type FC } from 'react'
|
||||
import { Modal, type ModalProps } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './index.css'
|
||||
const RbModal: FC<ModalProps> = ({
|
||||
onOk,
|
||||
onCancel,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, type FC, useCallback, useRef } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Input, type InputProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import searchIcon from '@/assets/images/search.svg'
|
||||
|
||||
@@ -11,6 +11,7 @@ interface SearchInputProps {
|
||||
defaultValue?: string;
|
||||
style?: Record<string, string | number>;
|
||||
className?: string;
|
||||
size?: InputProps['size']
|
||||
}
|
||||
|
||||
const SearchInput: FC<SearchInputProps> = ({
|
||||
@@ -79,7 +80,7 @@ const SearchInput: FC<SearchInputProps> = ({
|
||||
return (
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<img src={searchIcon} alt="search" className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
|
||||
prefix={<img src={searchIcon} alt="search" className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
placeholder={placeholder || t('user.searchPlaceholder')}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface TableComponentProps extends Omit<TableProps, 'pagination'> {
|
||||
rowSelection?: TableProps['rowSelection'];
|
||||
initialData?: Record<string, unknown>[];
|
||||
emptySize?: number;
|
||||
emptyText?: string;
|
||||
isScroll?: boolean;
|
||||
scrollX?: number | string | true; // 支持自定义横向滚动宽度
|
||||
scrollY?: number | string; // 支持自定义纵向滚动高度
|
||||
@@ -46,6 +47,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
|
||||
rowSelection,
|
||||
initialData,
|
||||
emptySize = 160,
|
||||
emptyText,
|
||||
isScroll = false,
|
||||
scrollX,
|
||||
scrollY,
|
||||
@@ -169,7 +171,7 @@ const TableComponent = forwardRef<TableRef, TableComponentProps>(({
|
||||
rowSelection={rowSelection}
|
||||
rowClassName={styles.row}
|
||||
className={styles.table}
|
||||
locale={{ emptyText: <Empty size={emptySize} /> }}
|
||||
locale={{ emptyText: <Empty size={emptySize} subTitle={emptyText} /> }}
|
||||
scroll={getScrollConfig()}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const en = {
|
||||
viewGuide: 'View Guide',
|
||||
watchVideo: 'Watch Video',
|
||||
viewDetails: 'View Details',
|
||||
changeLog: 'Change Log',
|
||||
changeLog: 'Changelog',
|
||||
latestUpdate: 'Latest Update',
|
||||
appCount: 'Number of Spaces',
|
||||
userCount: 'Number of Users',
|
||||
@@ -81,7 +81,7 @@ export const en = {
|
||||
userManagement: 'User Management',
|
||||
roleManagement: 'Role Management',
|
||||
systemManage: 'System Manage',
|
||||
applicationManagement: 'Application Manage',
|
||||
applicationManagement: 'Application',
|
||||
productManagement: 'Product Management',
|
||||
knowledgeManagement: 'KnowledgeBase ',
|
||||
knowledgeBase: 'Datasets Management',
|
||||
@@ -104,7 +104,7 @@ export const en = {
|
||||
knowledgeCreateDataset: 'Create Dataset',
|
||||
knowledgeDocumentDetails: 'Document Details',
|
||||
userMemoryDetail: 'UserMemory Detail',
|
||||
apiKeyManagement: 'API KEY Management',
|
||||
apiKeyManagement: 'API Key Management',
|
||||
toolManagement: 'Tool Management',
|
||||
emotionEngine: 'Emotion Engine',
|
||||
statementDetail: 'Emotion Memory',
|
||||
@@ -115,27 +115,27 @@ export const en = {
|
||||
spaceConfig: 'Space Configuration'
|
||||
},
|
||||
dashboard: {
|
||||
total_models: 'Total number of available models',
|
||||
total_spaces: 'Number of active spaces',
|
||||
total_users: 'Total number of users',
|
||||
total_running_apps: 'Number of application runs',
|
||||
desc_models: 'Contains {{ account }} LLMs and {{ nums }} Embeddings',
|
||||
desc_spaces: 'more than last week',
|
||||
total_models: 'Available Models',
|
||||
total_spaces: 'Active Spaces',
|
||||
total_users: 'Users',
|
||||
total_running_apps: 'Application Runs',
|
||||
desc_models: 'Includes {{ account }} LLM and {{ nums }} embedding',
|
||||
desc_spaces: 'compared to last week',
|
||||
desc_users: 'New additions this week',
|
||||
desc_running_apps: "Today's success rate",
|
||||
totalMemoryCapacity: 'Total Memory Capacity',
|
||||
totalMemoryCapacity: 'Total Stored Memories',
|
||||
userMemory: 'User Memory',
|
||||
knowledgeBaseCount: 'Knowledge Base Count',
|
||||
apiCallCount: 'API Call Count',
|
||||
knowledgeBaseCount: 'Knowledge Bases',
|
||||
apiCallCount: 'API Calls',
|
||||
comparedToYesterday: 'compared to yesterday',
|
||||
thisWeek: 'this week',
|
||||
thisDay: 'day on day',
|
||||
failureRate: 'Failure Rate',
|
||||
application: 'Application Count',
|
||||
application: 'Applications',
|
||||
total_num: 'Total Memory Capacity',
|
||||
|
||||
lastDays: 'last {{days}} days',
|
||||
lastHalfYear: 'last half year',
|
||||
lastHalfYear: 'last 6 months',
|
||||
lastYear: 'last year',
|
||||
|
||||
enterpriseMemory: 'Enterprise Memory',
|
||||
@@ -201,13 +201,13 @@ export const en = {
|
||||
tagEmpty: 'There are no tag records at the moment~',
|
||||
|
||||
chunk_count: 'Data Chunk',
|
||||
chunk_count_desc: 'Current Processing {{count}} Data Chunks',
|
||||
chunk_count_desc: '{{count}} data chunks currently being processed',
|
||||
statements_count: 'Statements',
|
||||
statements_count_desc: 'Manage {{count}} knowledge statements',
|
||||
statements_count_desc: '{{count}} knowledge statements',
|
||||
triplet_count: 'Entity Relation Extraction',
|
||||
triplet_count_desc: 'Build {{entities_count}} entity nodes and {{relations_count}} relation connections',
|
||||
triplet_count_desc: '{{entities_count}} entity nodes and {{relations_count}} relationships built',
|
||||
temporal_count: 'Time Extraction',
|
||||
temporal_count_desc: 'Record {{count}} time series information',
|
||||
temporal_count_desc: '{{count}} time series records',
|
||||
|
||||
dialogue: 'Dialogue',
|
||||
chunk: 'Chunk',
|
||||
@@ -417,7 +417,8 @@ export const en = {
|
||||
refresh: 'Refresh',
|
||||
return: 'Return',
|
||||
statusEnabled: 'Available',
|
||||
statusDisabled: 'Unavailable'
|
||||
statusDisabled: 'Unavailable',
|
||||
remove: 'Remove',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -947,20 +948,20 @@ export const en = {
|
||||
apiEndpoint: 'API Endpoint',
|
||||
apiKey: 'API-Key',
|
||||
returnToApplicationList: 'Return to application list',
|
||||
arrangement: 'Arrangement',
|
||||
arrangement: 'Configuration',
|
||||
api: 'API',
|
||||
release: 'Release',
|
||||
promptConfiguration: 'Prompt Configuration',
|
||||
configurationDesc: 'Define the role, capabilities, and behavioral guidelines of the Agent',
|
||||
aiPrompt: 'AI Prompt',
|
||||
promptPlaceholder: 'You are a professional AI assistant, and your responsibility is to help users solve problems.',
|
||||
knowledgeBaseAssociation: 'Knowledge base association',
|
||||
associatedKnowledgeBase: 'Associated Knowledge Base',
|
||||
knowledgeBaseAssociation: 'Knowledge Base Association',
|
||||
associatedKnowledgeBase: 'Associated Knowledge Bases',
|
||||
addKnowledgeBase: 'Add Knowledge Base',
|
||||
knowledgeEmpty: 'There is currently no knowledge base association',
|
||||
knowledgeEmpty: 'No knowledge base associated.',
|
||||
memoryConfiguration: 'Memory Configuration',
|
||||
dialogueHistoricalMemory: 'Dialogue Historical Memory',
|
||||
dialogueHistoricalMemoryDesc: 'After activation, the memory content in the memory management can be selected',
|
||||
dialogueHistoricalMemory: 'Conversation History Memory',
|
||||
dialogueHistoricalMemoryDesc: 'Enable this to select memory content from memory management.',
|
||||
toolConfiguration: 'Tool Configuration',
|
||||
webSearch: 'Web Search',
|
||||
webSearchDesc: 'Allow the Agent to access the Internet for real-time search',
|
||||
@@ -975,14 +976,14 @@ export const en = {
|
||||
VariableManagementDesc: 'Configure the available variables for the Agent',
|
||||
addVariables: 'Add Variables',
|
||||
variablesEmpty: 'There are currently no variables available',
|
||||
debuggingEmpty: 'Currently, there are no debugging models available',
|
||||
debuggingEmptyDesc: 'Click the "+" button on the page, select and add the model you need',
|
||||
debuggingAndPreview: 'Debugging and Preview',
|
||||
debuggingEmpty: 'No models available for debugging.',
|
||||
debuggingEmptyDesc: 'Click the “+” button to select and add a model.',
|
||||
debuggingAndPreview: 'Preview and Debugging',
|
||||
addModel: 'Add Model',
|
||||
fieldName: 'Field Name',
|
||||
Optional: 'Optional',
|
||||
chatEmpty: 'Send message to start testing',
|
||||
chatPlaceholder: 'Start chatting with the robot…',
|
||||
chatEmpty: 'Send a message to start testing',
|
||||
chatPlaceholder: 'Start a conversation...',
|
||||
|
||||
endpointConfiguration: 'Endpoint Configuration',
|
||||
authenticationMethod: 'Authentication Method',
|
||||
@@ -1009,22 +1010,22 @@ export const en = {
|
||||
whitelistIPDesc: 'supports CIDR',
|
||||
publicAPIDocumentation: 'Public API Documentation',
|
||||
|
||||
versionList: 'Version List',
|
||||
versionListDesc: 'All release records and status',
|
||||
versionList: 'Versions',
|
||||
versionListDesc: 'Release history and status',
|
||||
current: 'Current',
|
||||
rolledBack: 'rolled back',
|
||||
history: 'history',
|
||||
VersionInformation: 'Version Information',
|
||||
publishedOn: 'Published On',
|
||||
publisher: 'Publisher',
|
||||
DetailsOfVersion: 'Details of {{version}} version',
|
||||
publishedOn: 'Published on',
|
||||
publisher: 'Published by',
|
||||
DetailsOfVersion: 'Version Details: {{version}}',
|
||||
exportDSLFile: 'Export DSL file',
|
||||
willRollToThisVersion: 'Will roll to this version',
|
||||
share: 'Share',
|
||||
lastUpdateTime: 'Last Update Time',
|
||||
editor: 'Editor',
|
||||
releaseTime: 'Release Time',
|
||||
changeLog: 'Change Log',
|
||||
changeLog: 'Changelog',
|
||||
fix: 'Fix',
|
||||
optimization: 'Optimization',
|
||||
new: 'New',
|
||||
@@ -1129,7 +1130,7 @@ export const en = {
|
||||
ReplyException: 'Reply exception',
|
||||
|
||||
endpointConfigurationSubTitle: 'Configure API access address and supported HTTP methods',
|
||||
apiKeys: 'API Keys Management',
|
||||
apiKeys: 'API Key Management',
|
||||
apiKeySubTitle: 'Manage API keys, view usage and traffic statistics for each key',
|
||||
addApiKey: 'Add New API Key',
|
||||
apiKeyName: 'Key Name',
|
||||
@@ -1198,7 +1199,7 @@ export const en = {
|
||||
drag: 'Drag and drop to move nodes',
|
||||
zoom: 'Scroll zoom view',
|
||||
memoryDetailEmpty: 'Please select a memory node',
|
||||
memoryDetailEmptyDesc: 'Click on the node in the left graph to view the details of entity memory',
|
||||
memoryDetailEmptyDesc: 'Click a node in the graph to view memory details.',
|
||||
|
||||
totalNumOfMemories: 'Total Number of Memories',
|
||||
footprintCity: 'Footprint City',
|
||||
@@ -1216,20 +1217,20 @@ export const en = {
|
||||
editConfig: 'Edit Config',
|
||||
chooseModel: 'Choose Model',
|
||||
|
||||
nodeStatistics: 'Memory Classification',
|
||||
nodeStatistics: 'Memory Categories',
|
||||
total: 'Total',
|
||||
|
||||
PERCEPTUAL_MEMORY: 'Perceptual Memory',
|
||||
WORKING_MEMORY: 'Working Memory',
|
||||
SHORT_TERM_MEMORY: 'Shot Term Memory',
|
||||
LONG_TERM_MEMORY: 'Long Term Memory',
|
||||
SHORT_TERM_MEMORY: 'Short-Term Memory',
|
||||
LONG_TERM_MEMORY: 'Long-Term Memory',
|
||||
EXPLICIT_MEMORY: 'Explicit Memory',
|
||||
IMPLICIT_MEMORY: 'Implicit Memory',
|
||||
EMOTIONAL_MEMORY: 'Emotional Memory',
|
||||
EPISODIC_MEMORY: 'Episodic Memory',
|
||||
FORGET_MEMORY: 'Forget Memory',
|
||||
|
||||
endUserProfile: 'Core Profile',
|
||||
endUserProfile: 'Profile',
|
||||
editEndUserProfile: 'Edit',
|
||||
other_name: 'Name',
|
||||
position: 'Position',
|
||||
@@ -1239,10 +1240,10 @@ export const en = {
|
||||
hire_date: 'Hire Date',
|
||||
memoryContent: 'Memory Content',
|
||||
created_at: 'Created At',
|
||||
updated_at: 'Updated At',
|
||||
updated_at: 'Last Updated',
|
||||
fullScreen: 'Full Screen',
|
||||
|
||||
memoryWindow: "{{name}}'s Window of Memory",
|
||||
memoryWindow: "{{name}}'s Memory Overview",
|
||||
memory_insight: 'Overall Overview',
|
||||
key_findings: 'Key Findings',
|
||||
behavior_pattern: 'Behavior Pattern',
|
||||
@@ -1315,7 +1316,7 @@ export const en = {
|
||||
tTypeStrict: 'Type Matching Threshold',
|
||||
tOverall: 'Comprehensive Matching Threshold',
|
||||
|
||||
arrangementLayerModule: 'Arrangement Layer Module',
|
||||
arrangementLayerModule: 'Configuration Layer Module',
|
||||
queryMode: 'Query Mode',
|
||||
queryModeSubTitle: 'Control whether to activate deeper search functions',
|
||||
deepRetrieval: 'Deep Retrieval',
|
||||
@@ -1452,12 +1453,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total'
|
||||
},
|
||||
memoryConversation: {
|
||||
searchPlaceholder: 'Enter user ID...',
|
||||
chatEmpty:'Is there anything I can help you with?',
|
||||
searchPlaceholder: 'Input user ID...',
|
||||
userID: 'User ID',
|
||||
testMemoryConversation: 'Test Memory Conversation',
|
||||
conversationContent: 'Conversation Content',
|
||||
conversationContentEmpty: 'There is currently no conversation content available',
|
||||
conversationContentEmpty: 'No conversation content available.',
|
||||
memoryConversationAnalysis: 'Memory Conversation Analysis',
|
||||
memoryFunction: 'Memory Function',
|
||||
onlineSearch: 'Online Search',
|
||||
@@ -1477,8 +1478,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
quickReply: 'Quick Reply',
|
||||
web_search: 'Online search',
|
||||
memory: 'Memory',
|
||||
memoryConversationAnalysisEmpty: 'There is currently no dialogue analysis content available',
|
||||
memoryConversationAnalysisEmptySubTitle: 'After entering your user ID, click on "Test Memory" to view the conversation memory',
|
||||
memoryConversationAnalysisEmpty: 'No conversation analysis available.',
|
||||
memoryConversationAnalysisEmptySubTitle: 'Conversation analysis will appear here.',
|
||||
},
|
||||
login: {
|
||||
title: 'Red Bear Memory Science',
|
||||
@@ -1531,7 +1532,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
notFoundDesc: 'Try returning to the previous page',
|
||||
noPermission: 'Oh, this is an exclusive domain for permissions',
|
||||
noPermissionDesc: ' Please contact the administrator to grant permission',
|
||||
tableEmpty: 'There are currently no data',
|
||||
tableEmpty: 'No data available.',
|
||||
loadingEmpty: 'The content is loading…',
|
||||
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
|
||||
},
|
||||
@@ -1561,12 +1562,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
inactive: 'Expired'
|
||||
},
|
||||
tool: {
|
||||
mcp: 'MCP Service',
|
||||
mcp: 'MCP Services',
|
||||
inner: 'Built-in Tools',
|
||||
custom: 'Custom Tools',
|
||||
mcpSearchPlaceholder: 'Search MCP services...',
|
||||
innerSearchPlaceholder: 'Search tools...',
|
||||
customSearchPlaceholder: 'Search custom tools...',
|
||||
mcpSearchPlaceholder: 'Search MCP Services...',
|
||||
innerSearchPlaceholder: 'Search Tools...',
|
||||
customSearchPlaceholder: 'Search Custom Tools...',
|
||||
addService: 'Add MCP Service',
|
||||
editService: 'Edit MCP Service',
|
||||
addServiceSuccess: 'Service added successfully',
|
||||
@@ -1599,7 +1600,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
requestHeader: 'Request Headers',
|
||||
config: 'Configuration',
|
||||
auth_type: 'Authentication Type',
|
||||
none: 'No Authentication',
|
||||
none: 'None',
|
||||
api_key: 'API Key',
|
||||
basic_auth: 'Basic Auth',
|
||||
bearer_token: 'Bearer Token',
|
||||
@@ -1727,9 +1728,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
created_at: 'Created At',
|
||||
headerName: 'Header Name',
|
||||
null: 'None',
|
||||
tagDesc: 'Multiple tags separated by commas',
|
||||
tagDesc: 'Enter tags (comma-separated)',
|
||||
availableTools: 'Available Tools',
|
||||
name: 'Name',
|
||||
enterNamePlaceholder: 'Please enter a name',
|
||||
toolEmpty: 'No tools detected.',
|
||||
desc: 'Description',
|
||||
method: 'Method',
|
||||
path: 'Path',
|
||||
@@ -1796,9 +1799,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
temperature: 'Temperature',
|
||||
max_tokens: 'Max Tokens',
|
||||
context: 'Context',
|
||||
contextPlaceholder: '{x} Set Variable',
|
||||
memory: 'Memory',
|
||||
enable_window: 'Memory Window',
|
||||
inner: 'Built-in',
|
||||
messagesPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
},
|
||||
start: {
|
||||
variables: 'Input Fields',
|
||||
@@ -1809,7 +1814,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
array: 'Dropdown Options',
|
||||
object: 'Object',
|
||||
|
||||
addVariable: 'Add Variable',
|
||||
editVariable: 'Edit Variable',
|
||||
variableType: 'Variable Type',
|
||||
variableName: 'Variable Name',
|
||||
@@ -1833,6 +1837,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'parameter-extractor': {
|
||||
model_id: 'Model',
|
||||
text: 'Input Variable',
|
||||
textPlaceholder: '{x} Set Variable',
|
||||
params: 'Extract Parameters',
|
||||
prompt: 'Instruction',
|
||||
|
||||
@@ -1853,6 +1858,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
addParams: 'Add Extract Variable',
|
||||
promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: 'Aggregation Group',
|
||||
@@ -1922,6 +1929,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
loop: {
|
||||
cycle_vars: 'Loop Variables',
|
||||
condition: 'Loop Termination Condition',
|
||||
addCondition: 'Add Condition',
|
||||
max_loop: 'Maximum Loop Count',
|
||||
},
|
||||
assigner: {
|
||||
@@ -1958,6 +1966,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
addCase: 'Add Condition',
|
||||
addVariable: 'Add Variables',
|
||||
},
|
||||
|
||||
clear: 'Clear',
|
||||
@@ -2150,34 +2159,34 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
personal: {
|
||||
type: 'Personal',
|
||||
label: 'Current Package',
|
||||
typeDesc: 'For individuals',
|
||||
typeDesc: 'For Individuals',
|
||||
solution: "A person's second brain, capable of storing up to 2000 memories.",
|
||||
targetAudience: 'individual users, students, and first-time users',
|
||||
priceDesc: '/Forever free',
|
||||
priceDesc: '/Free forever',
|
||||
supportServices: 'Community Forum + Email Support',
|
||||
},
|
||||
team: {
|
||||
type: 'Team',
|
||||
label: 'Small Team',
|
||||
typeDesc: 'Small Team Version',
|
||||
typeDesc: 'For Small Teams',
|
||||
solution: "Enable every team to build a shared second brain in seconds.",
|
||||
targetAudience: 'Small teams, early-stage startups, and small projects.',
|
||||
priceDesc: '/Month',
|
||||
priceDesc: '/month',
|
||||
supportServices: 'Standard customer service support',
|
||||
},
|
||||
biz: {
|
||||
type: 'Biz+',
|
||||
label: 'Most Popular',
|
||||
typeDesc: 'Enterprise Growth Edition',
|
||||
typeDesc: 'Enterprise Growth Plan',
|
||||
solution: "Scale your organization with a powerful, enterprise-ready second-brain system.",
|
||||
targetAudience: 'Growing teams, startups, and SMBs requiring advanced memory capabilities.',
|
||||
priceDesc: '/Month/workspace',
|
||||
priceDesc: '/per workspace / month',
|
||||
supportServices: 'Priority customer service support',
|
||||
},
|
||||
commerce: {
|
||||
type: 'Commerce',
|
||||
label: 'Commercial OEM',
|
||||
typeDesc: 'Commercial OEM version',
|
||||
typeDesc: 'Commercial OEM plan',
|
||||
solution: "Seamlessly integrate advanced memory capabilities into your SaaS or enterprise product.",
|
||||
targetAudience: 'Large enterprises, SaaS vendors, and system integrators requiring fully customizable and secure deployment.',
|
||||
priceDesc: 'On-premises deployment',
|
||||
@@ -2196,8 +2205,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
supportServices: 'Support Services:',
|
||||
flexibleDeployment: 'Flexible deployment:',
|
||||
reliableGuarantee: 'Reliable guarantee:',
|
||||
alertTitle: 'Intellectual Property Authorization Reminder',
|
||||
alertContent: 'Please note: Using certain AI models (such as GPT-4, Claude, etc.) may involve third-party API call fees, which are not included in the Memory Bear platform subscription fee. You need to pay the relevant fees separately to the model provider. Memory Bear only charges platform management and service fees and does not bear the usage fees of third-party APIs.',
|
||||
alertTitle: 'Third-Party API Usage Notice',
|
||||
alertContent: "Please note: Some AI models(such as GPT- 4, Claude, etc.) may incur third- party API usage fees.These fees are not included in your Memory Bear subscription and must be paid directly to the model provider. Memory Bear charges only for platform management and related services and does not cover or assume responsibility for any third- party API usage fees.",
|
||||
currentAccountType: 'Current Account Type',
|
||||
validUntil: 'Valid Until',
|
||||
orderHistory: 'Order History',
|
||||
|
||||
@@ -509,7 +509,7 @@ export const zh = {
|
||||
VersionInformation: '版本信息',
|
||||
publishedOn: '发布于',
|
||||
publisher: '发布者',
|
||||
DetailsOfVersion: 'v{{version}}版本详情',
|
||||
DetailsOfVersion: '{{version}}版本详情',
|
||||
exportDSLFile: '导出DSL文件',
|
||||
willRollToThisVersion: '将回滚到此版本',
|
||||
share: '分享',
|
||||
@@ -965,7 +965,8 @@ export const zh = {
|
||||
refresh: '刷新',
|
||||
return: '返回',
|
||||
statusEnabled: '可用',
|
||||
statusDisabled: '不可用'
|
||||
statusDisabled: '不可用',
|
||||
remove: '删除',
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1823,6 +1824,8 @@ export const zh = {
|
||||
tagDesc: '多个标签用逗号分隔',
|
||||
availableTools: '可用工具',
|
||||
name: '名称',
|
||||
enterNamePlaceholder: '请输入名称',
|
||||
toolEmpty: '未检测到工具',
|
||||
desc: '描述',
|
||||
method: '方法',
|
||||
path: '路径',
|
||||
@@ -1889,9 +1892,11 @@ export const zh = {
|
||||
temperature: '温度',
|
||||
max_tokens: '最大令牌数',
|
||||
context: '上下文',
|
||||
contextPlaceholder: '{x} 设置变量',
|
||||
memory: '记忆',
|
||||
enable_window: '记忆窗口',
|
||||
inner: '内置',
|
||||
messagesPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
},
|
||||
start: {
|
||||
variables: '输入字段',
|
||||
@@ -1902,7 +1907,6 @@ export const zh = {
|
||||
array: '下拉选项',
|
||||
object: '对象',
|
||||
|
||||
addVariable: '添加变量',
|
||||
editVariable: '编辑变量',
|
||||
variableType: '变量类型',
|
||||
variableName: '变量名称',
|
||||
@@ -1922,10 +1926,12 @@ export const zh = {
|
||||
query: '查询变量',
|
||||
knowledge_retrieval: '知识库',
|
||||
recallConfig: '召回测试',
|
||||
addKnowledge: '添加知识库'
|
||||
},
|
||||
'parameter-extractor': {
|
||||
model_id: '模型',
|
||||
text: '输入变量',
|
||||
textPlaceholder: '{x} 设置变量',
|
||||
params: '提取参数',
|
||||
prompt: '指令',
|
||||
|
||||
@@ -1946,6 +1952,8 @@ export const zh = {
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
addParams: '添加提取变量',
|
||||
promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: '聚合分组',
|
||||
@@ -2015,6 +2023,7 @@ export const zh = {
|
||||
loop: {
|
||||
cycle_vars: '循环变量',
|
||||
condition: '循环终止条件',
|
||||
addCondition: '添加条件',
|
||||
max_loop: '最大循环次数',
|
||||
},
|
||||
assigner: {
|
||||
@@ -2051,6 +2060,7 @@ export const zh = {
|
||||
type: '类型',
|
||||
value: '值',
|
||||
addCase: '添加条件',
|
||||
addVariable: '添加变量',
|
||||
},
|
||||
|
||||
clear: '清空',
|
||||
|
||||
@@ -21,6 +21,8 @@ export const lightTheme: ThemeConfig = {
|
||||
colorBorderSecondary: '#DFE4ED',
|
||||
// colorBgContainer: '#FBFDFF',
|
||||
colorError: '#FF5D34',
|
||||
sizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
@@ -86,6 +88,7 @@ export const lightTheme: ThemeConfig = {
|
||||
rowSelectedBg: '#E9F1FF',
|
||||
rowSelectedHoverBg: '#F0F3F8',
|
||||
cellPaddingBlock: 8,
|
||||
cellFontSizeSM: 12,
|
||||
|
||||
// cellPaddingInline: 24,
|
||||
selectionColumnWidth: 48,
|
||||
@@ -95,6 +98,13 @@ export const lightTheme: ThemeConfig = {
|
||||
lastItemColor: '#212332',
|
||||
linkColor: '#5B6167',
|
||||
linkHoverColor: '#212332',
|
||||
},
|
||||
Input: {
|
||||
inputFontSizeSM: 12,
|
||||
controlHeightSM: 26
|
||||
},
|
||||
Select: {
|
||||
lineHeightSM: 26
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cookieUtils } from './request'
|
||||
export const clearAuthData = () => {
|
||||
console.log("Clearing auth data and redirecting to login");
|
||||
sessionStorage.clear();
|
||||
localStorage.clear()
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('breadcrumbs')
|
||||
cookieUtils.clear();
|
||||
}
|
||||
@@ -445,7 +445,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
|
||||
<Space size={10}>
|
||||
<Button type="primary" ghost onClick={handleAddModel}>
|
||||
+{t('application.addModel')}
|
||||
+ {t('application.addModel')}
|
||||
</Button>
|
||||
<div className="rb:w-8 rb:h-8 rb:cursor-pointer rb:bg-[url('@/assets/images/application/clean.svg')]" onClick={handleClearDebugging}></div>
|
||||
</Space>
|
||||
|
||||
@@ -47,11 +47,11 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
}
|
||||
return (
|
||||
<div className="rb:flex rb:h-[calc(100vh-64px)]">
|
||||
<div className="rb:h-full rb:overflow-y-auto rb:w-[432px] rb:flex-[0_0_auto] rb:border-r-[1px] rb:border-[#DFE4ED] rb:p-[16px]">
|
||||
<div className="rb:h-full rb:overflow-y-auto rb:w-108 rb:flex-[0_0_auto] rb:border-r rb:border-[#DFE4ED] rb:p-4">
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
<div className="rb:leading-[22px] rb:px-[4px]">
|
||||
<div className="rb:leading-5.5 rb:px-1">
|
||||
{t('application.versionList')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[4px] rb:leading-[16px]">{t('application.versionListDesc')}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:leading-4">{t('application.versionListDesc')}</div>
|
||||
</div>
|
||||
{releaseList.length === 0
|
||||
? <Empty />
|
||||
@@ -64,8 +64,8 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
<RbCard
|
||||
key={version.version}
|
||||
title={<>
|
||||
{version.version_name || `v${version.version}`}
|
||||
{tagKey && <Tag color={tagColors[tagKey]} className="rb:ml-[8px]">
|
||||
{version.version_name && version.version_name[0].toLocaleLowerCase() === 'v' ? version.version_name : version.version_name ? `v${version.version_name}` : `v${version.version}`}
|
||||
{tagKey && <Tag color={tagColors[tagKey]} className="rb:ml-2">
|
||||
{tagKey}
|
||||
</Tag>}
|
||||
</>}
|
||||
@@ -76,13 +76,13 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
headerType="borderless"
|
||||
onClick={() => setSelectedVersion(version)}
|
||||
>
|
||||
<div className="rb:leading-[20px] rb:line-clamp-2 rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
|
||||
<div className="rb:leading-5 rb:line-clamp-2 rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
|
||||
<Markdown content={version.release_notes} />
|
||||
</div>
|
||||
<div className="rb:mt-[16px] rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-4">
|
||||
{t('application.publishedOn')} {formatDateTime(version.published_at, 'YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[4px] rb:leading-[16px]">
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:leading-4">
|
||||
{t('application.publisher')}: {version.publisher_name}
|
||||
</div>
|
||||
</RbCard>
|
||||
@@ -91,13 +91,13 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
<div className="rb:h-full rb:overflow-y-auto rb:flex-[1_1_auto] rb:p-[16px]">
|
||||
<div className="rb:h-full rb:overflow-y-auto rb:flex-[1_1_auto] rb:p-4">
|
||||
<Form layout="vertical">
|
||||
<div className={clsx("rb:leading-[22px] rb:px-[4px] rb:flex rb:items-center rb:text-[16px] rb:font-medium rb:mb-[21px]", {
|
||||
<div className={clsx("rb:leading-5.5 rb:px-1 rb:flex rb:items-center rb:text-[16px] rb:font-medium rb:mb-5.25", {
|
||||
'rb:justify-between': selectedVersion,
|
||||
'rb:justify-end': !selectedVersion
|
||||
})}>
|
||||
{selectedVersion && t('application.DetailsOfVersion', { version: selectedVersion.version_name || `v${selectedVersion.version}` || '-' })}
|
||||
{selectedVersion && t('application.DetailsOfVersion', { version: selectedVersion.version_name && selectedVersion.version_name[0].toLocaleLowerCase() === 'v' ? selectedVersion.version_name : selectedVersion.version_name ? `v${selectedVersion.version_name}` : `v${selectedVersion.version}` || '-' })}
|
||||
|
||||
<Space size={10}>
|
||||
{selectedVersion && <>
|
||||
@@ -111,14 +111,14 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
{selectedVersion &&
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
<RbCard title={t('application.VersionInformation')} headerType="borderless">
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-[16px]">
|
||||
<Form.Item label={t('application.releaseTime')} className="rb:mb-[0]!">
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
<Form.Item label={t('application.releaseTime')} className="rb:mb-0!">
|
||||
<Input value={formatDateTime(selectedVersion.published_at, 'YYYY-MM-DD HH:mm:ss')} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('application.lastUpdateTime')} className="rb:mb-[0]!">
|
||||
<Form.Item label={t('application.lastUpdateTime')} className="rb:mb-0!">
|
||||
<Input value={formatDateTime(selectedVersion.updated_at, 'YYYY-MM-DD HH:mm:ss')} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('application.editor')} className="rb:mb-[0]!">
|
||||
<Form.Item label={t('application.editor')} className="rb:mb-0!">
|
||||
<Input value={selectedVersion.publisher_name} disabled />
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -131,9 +131,9 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
|
||||
<RbCard
|
||||
headerType="borderBL"
|
||||
title={<div className="rb:text-[14px]">{formatDateTime(selectedVersion.published_at, 'YYYY-MM-DD HH:mm:ss')}</div>}
|
||||
extra={<span className="rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">{selectedVersion.publisher_name}</span>}
|
||||
extra={<span className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4">{selectedVersion.publisher_name}</span>}
|
||||
>
|
||||
<div className="rb:leading-[20px] rb:font-medium rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">
|
||||
<div className="rb:font-medium rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-4">
|
||||
<Markdown content={selectedVersion.release_notes} />
|
||||
</div>
|
||||
</RbCard>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { type FC, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Layout, Tabs, Dropdown, Button } from 'antd';
|
||||
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from '../index.module.css'
|
||||
@@ -141,10 +141,12 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
||||
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
|
||||
</div>
|
||||
: <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
: <Flex justify="flex-end">
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
</Header>
|
||||
<ApplicationModal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, Slider, Button, Alert, message, Switch, Space } from 'antd';
|
||||
import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
@@ -9,6 +9,7 @@ import type { ConfigForm } from './types'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import Tag from '@/components/Tag'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
{
|
||||
@@ -158,23 +159,17 @@ const EmotionEngine: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`emotionEngine.${config.key}`)}</span>
|
||||
<SwitchFormItem
|
||||
title={t(`emotionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>}
|
||||
className="rb:mb-6"
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, Slider, Button, Space, message, Switch } from 'antd';
|
||||
import { Row, Col, Form, Slider, Button, Space, message } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
@@ -7,6 +7,7 @@ import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimula
|
||||
import LineChart from './components/LineChart'
|
||||
import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory'
|
||||
import type { ConfigForm } from './types'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
{
|
||||
@@ -155,26 +156,12 @@ const ForgettingEngine: React.FC = () => {
|
||||
{configList.map(config => {
|
||||
if (config.type === 'button') {
|
||||
return (
|
||||
<div key={config.key} className="rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`forgettingEngine.${config.key}`)}</span>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.name}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5">
|
||||
<Space size={4}>
|
||||
{config.range && <span>{t(`forgettingEngine.range`)}: {config.range?.join('-')}</span>}
|
||||
{config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<SwitchFormItem
|
||||
title={t(`forgettingEngine.${config.key}`)}
|
||||
name={config.name}
|
||||
desc={config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
|
||||
className="rb:mb-2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
@@ -191,8 +178,6 @@ const ForgettingEngine: React.FC = () => {
|
||||
>
|
||||
{config.type === 'decimal'
|
||||
? <Slider tooltip={{ open: false }} max={config.range?.[1] || 1} min={config.range?.[0] || 0} step={config.step ?? 0.01} style={{ margin: '0' }} />
|
||||
: config.type === 'button'
|
||||
? <Switch />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -77,7 +77,8 @@ const TopCardList: FC<{data?: DataResponse}> = ({ data }) => {
|
||||
|
||||
<div className={styles.content}>
|
||||
{item.key === 'spaces' && String(data?.active_workspaces)}
|
||||
{item.key !== 'spaces' && String(data?.[`total_${item.key}` as keyof DataResponse] || item.value || 0)}
|
||||
{item.key === 'running_apps' && String(data?.[`${item.key}` as keyof DataResponse] || item.value || 0)}
|
||||
{item.key !== 'spaces' && item.key !== 'running_apps' && String(data?.[`total_${item.key}` as keyof DataResponse] || item.value || 0)}
|
||||
</div>
|
||||
<div className='rb:flex rb:flex-col rb:items-start'>
|
||||
{item.key === 'models' ? (
|
||||
|
||||
@@ -4,29 +4,32 @@
|
||||
* @Author: yujiangping
|
||||
* @Date: 2026-01-12 16:34:59
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2026-01-13 19:14:30
|
||||
* @LastEditTime: 2026-01-16 15:38:35
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider } from 'antd';
|
||||
import { Divider } from 'antd';
|
||||
// import arrowRight from '@/assets/images/index/arrow_right.svg'
|
||||
import { getVersion, type versionResponse } from '@/api/common'
|
||||
|
||||
const GuideCard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [versionInfo, setVersionInfo] = useState<versionResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取当前语言对应的介绍信息
|
||||
const getIntroduction = () => {
|
||||
if (!versionInfo) return null;
|
||||
const currentLang = i18n.language;
|
||||
return currentLang === 'zh' ? versionInfo.introduction : (versionInfo.introduction_en || versionInfo.introduction);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getVersion();
|
||||
setVersionInfo(response);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch version:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,27 +44,30 @@ const GuideCard: React.FC = () => {
|
||||
{versionInfo?.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className='rb:flex rb:flex-col rb:text-[#5B6167]'>
|
||||
{versionInfo && (<>
|
||||
<div className='rb:flex rb:items-center rb:gap-2 rb:text-sm rb:text-[#5B6167] rb:leading-5 '>
|
||||
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.releaseDate')}: {versionInfo.introduction?.releaseDate}
|
||||
</span>
|
||||
<Divider type='vertical' />
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.name')}: {versionInfo.introduction?.codeName}
|
||||
</span>
|
||||
</div>
|
||||
<p className='rb:text-sm rb:text-[#5B6167] rb:leading-5 rb:mt-2 '>
|
||||
{versionInfo.introduction?.upgradePosition}
|
||||
</p>
|
||||
{versionInfo.introduction?.coreUpgrades?.map((item,index) => (
|
||||
<p className='rb:text-sm rb:text-[#5B6167] rb:leading-5'>
|
||||
{index + 1}. {item}
|
||||
</p>
|
||||
))}
|
||||
</>)}
|
||||
<div className='rb:flex rb:flex-col rb:max-h-[400px] rb:overflow-y-auto rb:text-[#5B6167]'>
|
||||
{versionInfo && (() => {
|
||||
const introduction = getIntroduction();
|
||||
return introduction ? (<>
|
||||
<div className='rb:flex rb:items-center rb:gap-2 rb:text-sm rb:text-[#5B6167] rb:leading-5 '>
|
||||
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.releaseDate')}: {introduction.releaseDate}
|
||||
</span>
|
||||
<Divider type='vertical' />
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.name')}: {introduction.codeName}
|
||||
</span>
|
||||
</div>
|
||||
<p className='rb:text-sm rb:text-[#5B6167] rb:leading-5 rb:mt-2 '>
|
||||
{introduction.upgradePosition}
|
||||
</p>
|
||||
{introduction.coreUpgrades?.map((item: string, index: number) => (
|
||||
<p key={index} className='rb:text-sm rb:text-[#5B6167] rb:leading-5'>
|
||||
{index + 1}. {item}
|
||||
</p>
|
||||
))}
|
||||
</>) : null;
|
||||
})()}
|
||||
{/* {loading ? (
|
||||
t('index.loading')
|
||||
) : (
|
||||
|
||||
@@ -151,7 +151,7 @@ const MemoryConversation: FC = () => {
|
||||
>
|
||||
<Chat
|
||||
empty={
|
||||
<Empty url={ConversationEmptyIcon} className="rb:h-full" size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
<Empty url={ConversationEmptyIcon} className="rb:h-full" size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} isNeedSubTitle={false} />
|
||||
}
|
||||
contentClassName='rb:h-[calc(100vh-362px)]'
|
||||
data={chatData}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const keys = [
|
||||
// 'example',
|
||||
@@ -173,25 +174,18 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px]">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
|
||||
{vo.list.map(config => (
|
||||
<div key={config.label}>
|
||||
{config.control === 'button' &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-2" />
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<SwitchFormItem
|
||||
title={<>-{t(`memoryExtractionEngine.${config.label}`)}</>}
|
||||
name={config.variableName}
|
||||
desc={<ConfigDesc config={config} className="rb:ml-2" />}
|
||||
className="rb:mt-6"
|
||||
/>
|
||||
}
|
||||
{config.control === 'select' &&
|
||||
<>
|
||||
|
||||
@@ -67,8 +67,8 @@ const OrderDetail = forwardRef<OrderDetailRef, { getProductType: (type: string)
|
||||
onCancel={handleClose}
|
||||
width={1000}
|
||||
>
|
||||
<Descriptions title={t('pricing.orderInfo')} column={2} items={formatItems()} classNames={{ label: 'rb:w-40' }} />
|
||||
<Descriptions title={t('pricing.orderPayInfo')} column={2} items={formatPayItems()} classNames={{ label: 'rb:w-40' }} className="rb:mt-6!" />
|
||||
<Descriptions title={t('pricing.orderInfo')} column={2} items={formatItems()} classNames={{ label: 'rb:w-50' }} />
|
||||
<Descriptions title={t('pricing.orderPayInfo')} column={2} items={formatPayItems()} classNames={{ label: 'rb:w-50' }} className="rb:mt-6!" />
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd';
|
||||
import { Row, Col, Form, App, Button, Space, Select } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -11,6 +11,7 @@ import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import Tag from '@/components/Tag'
|
||||
import { useI18n } from '@/store/locale';
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
// 启用反思引擎
|
||||
@@ -219,21 +220,16 @@ const SelfReflectionEngine: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`reflectionEngine.${config.key}`)}</span>
|
||||
<SwitchFormItem
|
||||
title={t(`reflectionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{(config as any).hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_desc`)}</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch
|
||||
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>}
|
||||
className="rb:mb-6"
|
||||
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, Row, Col, App, Button } from 'antd';
|
||||
import { Form, Input, Select, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomToolItem, CustomToolModalRef, ToolItem } from '../types'
|
||||
@@ -134,9 +134,9 @@ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('tool.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
rules={[{ required: true, message: t('common.enterNamePlaceholder') }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input placeholder={t('tool.enterNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
{/* 名称和图标 */}
|
||||
{/* <Form.Item label={t('tool.nameAndIcon')} required>
|
||||
@@ -195,6 +195,7 @@ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
|
||||
]}
|
||||
initialData={parseSchemaData.operations || []}
|
||||
emptySize={88}
|
||||
emptyText={t('tool.toolEmpty')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -269,7 +270,7 @@ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
placeholder={t('tool.tagDesc')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
@@ -60,25 +60,23 @@ const InnerToolModal = forwardRef<InnerToolModalRef, InnerToolModalProps>(({
|
||||
...values.config,
|
||||
}
|
||||
} as any)
|
||||
.then(() => {
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
refreshTable()
|
||||
.then((res: any) => {
|
||||
message.success(t('common.saveSuccess'));
|
||||
testConnection(res.tool_id || editVo?.id)
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
handleClose();
|
||||
refreshTable()
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleTestConnection = () => {
|
||||
testConnection(editVo.id)
|
||||
.then(() => {
|
||||
message.success(t('tool.testConnectionSuccess'));
|
||||
})
|
||||
.finally(() => {
|
||||
refreshTable()
|
||||
})
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -91,12 +89,9 @@ const InnerToolModal = forwardRef<InnerToolModalRef, InnerToolModalProps>(({
|
||||
title={`${editVo.name} ${t('tool.config')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('tool.saveAndTest')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
footer={[
|
||||
<Button onClick={handleClose}>{t('common.cancel')}</Button>,
|
||||
<Button onClick={handleTestConnection}>{t('tool.textLink')}</Button>,
|
||||
<Button type="primary" loading={loading} onClick={handleSave}>{t('common.save')}</Button>,
|
||||
]}
|
||||
>
|
||||
{editVo?.config_data?.tool_class && config && <>
|
||||
<RbAlert className="rb:mb-3">
|
||||
|
||||
@@ -2,12 +2,13 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Checkbox } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { StartVariableItem, VariableConfigModalRef } from '../../types'
|
||||
import type { VariableConfigModalRef } from '../../types'
|
||||
import type { Variable } from '../Properties/VariableList/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refresh: (values: StartVariableItem[]) => void;
|
||||
variables: StartVariableItem[]
|
||||
refresh: (values: Variable[]) => void;
|
||||
variables: Variable[]
|
||||
}
|
||||
|
||||
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({
|
||||
@@ -15,9 +16,9 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{variables: StartVariableItem[]}>();
|
||||
const [form] = Form.useForm<{variables: Variable[]}>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([])
|
||||
const [initialValues, setInitialValues] = useState<Variable[]>([])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -26,7 +27,7 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (values: StartVariableItem[]) => {
|
||||
const handleOpen = (values: Variable[]) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({variables: values})
|
||||
setInitialValues([...values])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { type FC, useState, useEffect, useMemo } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
@@ -25,7 +25,11 @@ interface LexicalEditorProps {
|
||||
options: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
enableJinja2?: boolean;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea'
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -51,8 +55,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
onChange,
|
||||
options,
|
||||
variant = 'borderless',
|
||||
height = 60,
|
||||
enableJinja2 = false,
|
||||
size = 'default',
|
||||
type = 'textarea'
|
||||
}) => {
|
||||
|
||||
const [_count, setCount] = useState(0);
|
||||
@@ -94,12 +99,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
display: flex;
|
||||
}
|
||||
.line-numbers {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
color: #656d76;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
line-height: 16px;
|
||||
padding: 4px 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
@@ -142,6 +144,21 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
console.error(error);
|
||||
},
|
||||
};
|
||||
const minheight = useMemo(() => {
|
||||
if (type === 'input') {
|
||||
return `${size === 'small' ? 26 : 30}px`
|
||||
}
|
||||
return `${size === 'small' ? 60 : 120}px`
|
||||
}, [type, size])
|
||||
const fontSize = useMemo(() => {
|
||||
return `${size === 'small' ? 12 : 14}px`
|
||||
}, [size])
|
||||
const lineHeight = useMemo(() => {
|
||||
return `${size === 'small' ? 16 : 20}px`
|
||||
}, [size])
|
||||
const placeHolderMinheight = useMemo(() => {
|
||||
return `${size === 'small' ? 16 : 30}px`
|
||||
}, [type, size])
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
@@ -152,7 +169,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<div className="editor-with-line-numbers" style={{
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
minHeight: `${height}px`,
|
||||
minHeight: minheight,
|
||||
}}>
|
||||
<div className="line-numbers">
|
||||
<div>1</div>
|
||||
@@ -160,12 +177,12 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<ContentEditable
|
||||
className="editor-content-with-numbers"
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: '4px 11px',
|
||||
minHeight: minheight,
|
||||
padding: '4px 0',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontSize: fontSize,
|
||||
lineHeight: lineHeight,
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
@@ -173,14 +190,14 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
) : (
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
minHeight: minheight,
|
||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontSize: fontSize,
|
||||
lineHeight: lineHeight,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -188,12 +205,13 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder={
|
||||
<div
|
||||
style={{
|
||||
minHeight: placeHolderMinheight,
|
||||
position: 'absolute',
|
||||
top: variant === 'borderless' ? '0' : '6px',
|
||||
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
color: '#5B6167',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
color: '#A8A9AA',
|
||||
fontSize: fontSize,
|
||||
lineHeight: placeHolderMinheight,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -9,12 +9,21 @@ const Jinja2HighlightPlugin = () => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (containsJinja2Patterns(text)) {
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
// Skip if node already has styling (prevent infinite recursion)
|
||||
if (textNode.getStyle()) return;
|
||||
|
||||
// Skip if no Jinja2 patterns found
|
||||
if (!containsJinja2Patterns(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const tokens = tokenizeJinja2(text);
|
||||
const newNodes = tokens.map(token => {
|
||||
const tokens = tokenizeJinja2(text);
|
||||
|
||||
// Skip if no meaningful tokenization (only one text token)
|
||||
if (tokens.length <= 1 || (tokens.length === 1 && tokens[0].type === 'text')) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
|
||||
switch (token.type) {
|
||||
@@ -30,16 +39,16 @@ const Jinja2HighlightPlugin = () => {
|
||||
newNode.setStyle('color: #008000');
|
||||
break;
|
||||
case 'brace-0':
|
||||
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #155EEF; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-1':
|
||||
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #369F21; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-2':
|
||||
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #FF5D34; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-3':
|
||||
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #5B6167; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'expression-0':
|
||||
case 'expression-1':
|
||||
@@ -77,7 +86,6 @@ const Jinja2HighlightPlugin = () => {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Row, Col, Select, InputNumber, Radio } from 'antd'
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Select, InputNumber, Radio, Button, Space } from 'antd'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -9,6 +8,7 @@ interface AssignmentListProps {
|
||||
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||
parentName: string;
|
||||
options: Suggestion[];
|
||||
size?: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const operationsObj = {
|
||||
@@ -31,6 +31,7 @@ const operationsObj = {
|
||||
const AssignmentList: FC<AssignmentListProps> = ({
|
||||
parentName,
|
||||
options = [],
|
||||
size = 'small'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -39,109 +40,126 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-between">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'variable_selector']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.') || (vo.nodeData.type === 'iteration' && (vo.label === 'item' || vo.label === 'index')))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'operation']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={operationOptions.map(op => ({
|
||||
...op,
|
||||
label: t(op.label)
|
||||
}))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.5">
|
||||
<div className="rb:text-[12px] rb:leading-4.5 rb:font-medium">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
</div>
|
||||
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
if (operation === 'clear') return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => add({ operation: 'cover' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start">
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:flex rb:gap-1 rb:mb-1">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
name={[name, 'variable_selector']}
|
||||
noStyle
|
||||
>
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
}
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.') || (vo.nodeData.type === 'iteration' && (vo.label === 'item' || vo.label === 'index')))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-39!"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'operation']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={operationOptions.map(op => ({
|
||||
...op,
|
||||
label: t(op.label)
|
||||
}))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-24!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
if (operation === 'clear') return null;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
/>
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
size={size}
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block size={size}>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
@@ -247,37 +246,40 @@ const CaseList: FC<CaseListProps> = ({
|
||||
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
|
||||
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
|
||||
return (
|
||||
<div className={clsx("rb:relative rb:mb-4 rb:border rb:border-gray-200 rb:rounded rb:p-3 rb:pl-5")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:font-medium">
|
||||
{caseIndex === 0 ? 'IF' : 'ELIF'}<br/>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]">{`CASE ${caseIndex + 1}`}</span>}
|
||||
</span>
|
||||
<div className={clsx("rb:relative")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:leading-4.5">
|
||||
<span className="rb:font-medium ">{caseIndex === 0 ? 'IF' : 'ELIF'}</span>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]"> ({`CASE ${caseIndex + 1}`})</span>}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => addCondition({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
{caseFields.length > 1 && <DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
/>}
|
||||
{caseFields.length > 1 &&
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
>
|
||||
{t('common.remove')}
|
||||
</Button>
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 &&
|
||||
<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
{conditionFields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const cases = form.getFieldValue(name) || [];
|
||||
const currentCase = cases[caseIndex] || {};
|
||||
@@ -290,91 +292,86 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
})}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
className="rb:bg-white! rb:w-29.5!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
className="rb:bg-white! rb:w-22!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{!hideRightField && <>
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{leftFieldType === 'number'
|
||||
? <Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
? <div className="rb:flex rb:items-center">
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
className="rb:w-18!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
: <Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Editor options={options} />
|
||||
: <Editor options={options} size="small" type="input" />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</>}
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -388,6 +385,8 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => handleAddCase(addCase)}
|
||||
>
|
||||
+ ELIF
|
||||
@@ -395,9 +394,9 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
<div className="rb:font-medium">ELSE</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] ">{t('workflow.config.if-else.else_desc')}</div>
|
||||
|
||||
<div className="rb:font-medium rb:text-[12px] rb:mt-4 rb:leading-4.5">ELSE</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-4.5">{t('workflow.config.if-else.else_desc')}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Space } from 'antd';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
|
||||
import Editor from '../../Editor';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
@@ -151,17 +151,15 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
const contentLength = (currentItem.class_name || '').length;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2 rb:bg-[#F8F9FB]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2">{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-1">
|
||||
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||
/>
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
@@ -172,6 +170,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
<Editor
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
options={options}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -179,8 +178,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
size="middle"
|
||||
block
|
||||
onClick={() => handleAddCategory(add)}
|
||||
className="rb:w-full"
|
||||
className="rb:text-[12px]!"
|
||||
>
|
||||
+ {t('workflow.config.question-classifier.addClassName')}
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Form, Button, Select, InputNumber, Radio, Input, Divider, type SelectProps } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
@@ -84,52 +83,64 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{(fields, { add, remove }) => {
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
{fields.map((field, index) => {
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:mb-3">
|
||||
{index > 0 && (<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
|
||||
<Row gutter={8} align="middle">
|
||||
<Col span={14}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.loop.condition')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.loop.addCondition')}
|
||||
</Button>
|
||||
</div>
|
||||
{fields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
{fields.map((field, index) => {
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
})}>
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options.filter(vo =>
|
||||
vo.value.includes('sys.') ||
|
||||
vo.value.includes('conv.') ||
|
||||
options={options.filter(vo =>
|
||||
vo.value.includes('sys.') ||
|
||||
vo.value.includes('conv.') ||
|
||||
vo.nodeData.type === 'loop' ||
|
||||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)
|
||||
)}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={(val) => handleLeftFieldChange(index, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name={[field.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
@@ -138,84 +149,67 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
</div>
|
||||
|
||||
{!hideRightField && <>
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{leftFieldType === 'number'
|
||||
? <Col span={24}><Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
? <div className="rb:flex rb:items-center">
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row></Col>
|
||||
: <Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</div>
|
||||
: <Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</>}
|
||||
|
||||
</Row>
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(field.name)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ left: '', operator: '', right: '' })}
|
||||
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||
icon={<span>+</span>}
|
||||
>
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Select, Row, Col, Input } from 'antd'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Select, Input, Button } from 'antd'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -20,6 +19,7 @@ interface CycleVarsListProps {
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
size?: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const types = [
|
||||
@@ -37,7 +37,8 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
options,
|
||||
parentName,
|
||||
selectedNode,
|
||||
graphRef
|
||||
graphRef,
|
||||
size = 'middle'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -78,62 +79,56 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
const availableOptions = getChildNodeVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...field }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white">
|
||||
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
// 重置 value 字段
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(name)}
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-[12px] rb:font-medium">{t('workflow.config.loop.cycle_vars')}</span>
|
||||
<Button
|
||||
onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map(({ key, name }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start rb:mb-2">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className="rb:flex rb:gap-1 rb:p-1 rb:border-b rb:border-b-[#DFE4ED]">
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size={size} className="rb:w-23!" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size={size}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-18.5!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size={size}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
className="rb:w-18!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
{currentInputType === 'variable' ? (
|
||||
@@ -145,22 +140,29 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
|
||||
return option.dataType === currentType
|
||||
})}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
variant="borderless"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col } from 'antd'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -9,13 +9,15 @@ interface GroupVariableListProps {
|
||||
value?: Array<{ key: string; value: string[]; }>;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
isCanAdd: boolean
|
||||
isCanAdd: boolean;
|
||||
size: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
name,
|
||||
options = [],
|
||||
isCanAdd = false
|
||||
isCanAdd = false,
|
||||
size = "middle"
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -54,6 +56,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={filteredOptions}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -76,11 +79,15 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} /> : t('workflow.config.var-aggregator.variable')}
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} size={size} /> : t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Col>}
|
||||
</Row>
|
||||
|
||||
@@ -104,16 +111,22 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
})()
|
||||
}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{isCanAdd && <Form.Item noStyle>
|
||||
<Button type="dashed" onClick={() => add({ key: `Group${fields.length + 1}` })} block>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>
|
||||
</Form.Item>}
|
||||
|
||||
{isCanAdd && <Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => add({ key: `Group${fields.length + 1}` })}
|
||||
>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -93,6 +93,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
initialValues={{
|
||||
auth: 'none'
|
||||
}}
|
||||
size="middle"
|
||||
>
|
||||
<FormItem
|
||||
name="auth"
|
||||
@@ -102,6 +103,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
size="middle"
|
||||
options={[
|
||||
{ value: 'none', label: t('workflow.config.http-request.none') },
|
||||
{ value: 'api_key', label: t('workflow.config.http-request.apiKey') },
|
||||
@@ -117,6 +119,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
size="middle"
|
||||
options={[
|
||||
{ value: 'basic', label: t('workflow.config.http-request.basic') },
|
||||
{ value: 'bearer', label: t('workflow.config.http-request.bearer') },
|
||||
@@ -132,7 +135,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
{ required: true, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input size="middle" placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
}
|
||||
<FormItem
|
||||
@@ -142,7 +145,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
{ required: true, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input size="middle" placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
</>}
|
||||
</Form>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import Empty from '@/components/Empty';
|
||||
import VariableSelect from '../VariableSelect';
|
||||
@@ -19,6 +18,7 @@ interface EditableTableProps {
|
||||
options?: Suggestion[];
|
||||
typeOptions?: { value: string, label: string }[]
|
||||
filterBooleanType?: boolean;
|
||||
size?: "small"
|
||||
}
|
||||
|
||||
const EditableTable: React.FC<EditableTableProps> = ({
|
||||
@@ -26,7 +26,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
title,
|
||||
options = [],
|
||||
typeOptions = [],
|
||||
filterBooleanType = false
|
||||
filterBooleanType = false,
|
||||
size = 'small'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -38,21 +39,24 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
|
||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
const baseWidth = hasType ? '35%' : '45%';
|
||||
const cellClassName="rb:p-1!"
|
||||
const contentClassName ="rb:w-[108px]! rb:text-[12px]!"
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
width: baseWidth,
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={options}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -61,18 +65,20 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
title: t('workflow.config.type'),
|
||||
dataIndex: 'type',
|
||||
width: '20%',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => (
|
||||
<Form.Item name={[index, 'type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={typeOptions}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -82,7 +88,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{
|
||||
title: t('workflow.config.value'),
|
||||
dataIndex: 'value',
|
||||
width: baseWidth,
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
@@ -102,10 +108,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<Form.Item name={[index, 'value']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={filteredOptions}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -116,9 +124,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'actions',
|
||||
width: '10%',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(index)}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -129,13 +140,11 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => {
|
||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||
<Button
|
||||
type={block ? "dashed" : "text"}
|
||||
<Button
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1" : ""}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
</Button>
|
||||
@@ -145,8 +154,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<>
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<AddButton />
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={true} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -161,8 +170,9 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
columns={getColumns(remove)}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowClassName="rb:p-0! rb:bg-[#F6F8FC]!"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
style={{ width: '274px' }}
|
||||
/>
|
||||
|
||||
{!title && <AddButton block />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type FC, useRef } from "react";
|
||||
import { type FC, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
|
||||
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import Editor from '../../Editor'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import AuthConfigModal from './AuthConfigModal'
|
||||
@@ -65,15 +66,23 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
}
|
||||
}
|
||||
|
||||
console.log('HttpRequest', values)
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const handleToggle = () => {
|
||||
setCollapsed((prev: boolean) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-4">
|
||||
<div>API</div>
|
||||
<Button onClick={handleChangeAuth}>{t('workflow.config.http-request.auth')}</Button>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-1">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
|
||||
<Button onClick={handleChangeAuth}
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
className="rb:mt-1 rb:text-[12px]!"
|
||||
>{t('workflow.config.http-request.auth')}: {!values?.auth?.auth_type || values?.auth?.auth_type === 'none' ? t('workflow.config.http-request.none') : t('workflow.config.http-request.apiKey')}</Button>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Row gutter={4}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="method">
|
||||
<Select
|
||||
@@ -85,35 +94,43 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
]}
|
||||
className="rb:bg-transparent!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item name="url">
|
||||
<Editor options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" />
|
||||
<Editor
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="auth" hidden>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="headers">
|
||||
<Form.Item name="headers" noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName="headers"
|
||||
title="HEADERS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="params">
|
||||
<Form.Item name="params" noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName="params"
|
||||
title="PARAMS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="BODY">
|
||||
<Form.Item label="BODY" className="rb:mb-0!">
|
||||
<Form.Item name={['body', 'content_type']}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
@@ -131,6 +148,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{values?.body?.content_type === 'form-data' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
typeOptions={[
|
||||
@@ -143,6 +161,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{values?.body?.content_type === 'x-www-form-urlencoded' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
filterBooleanType={true}
|
||||
@@ -150,7 +169,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'json' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
@@ -161,7 +180,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'raw' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<MessageEditor
|
||||
key="raw"
|
||||
parentName={['body', 'data']}
|
||||
@@ -172,7 +191,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'binary' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
@@ -182,15 +201,19 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
}
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')}>
|
||||
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')} className="rb:mb-0!">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
<div>{t('workflow.config.http-request.timeouts')}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5 rb:mb-2.5 rb:cursor-pointer" onClick={handleToggle}>
|
||||
{t('workflow.config.http-request.timeouts')}
|
||||
{collapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['timeouts', 'connect_timeout']}
|
||||
label={t('workflow.config.http-request.connect_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -201,6 +224,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item
|
||||
name={['timeouts', 'read_timeout']}
|
||||
label={t('workflow.config.http-request.read_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -211,6 +235,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item
|
||||
name={['timeouts', 'write_timeout']}
|
||||
label={t('workflow.config.http-request.write_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, List } from 'antd'
|
||||
import { Space, Button } from 'antd'
|
||||
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
|
||||
import type {
|
||||
KnowledgeConfigForm,
|
||||
@@ -113,49 +113,65 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{t('application.knowledgeBaseAssociation')}</div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('application.knowledgeBaseAssociation')}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
|
||||
</Space>
|
||||
<Button
|
||||
onClick={handleKnowledgeConfig}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!"
|
||||
size="small"
|
||||
>
|
||||
<div
|
||||
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
|
||||
></div>
|
||||
{t('workflow.config.knowledge-retrieval.recallConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={knowledgeList}
|
||||
renderItem={(item) => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<List.Item>
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={handleAddKnowledge}
|
||||
>
|
||||
+ {t('workflow.config.knowledge-retrieval.addKnowledge')}
|
||||
</Button>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
: knowledgeList.map(item => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<div key={item.id} className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:py-2 rb:px-2.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="">
|
||||
<span className="rb:font-medium rb:leading-4">{item.name}</span>
|
||||
<Tag
|
||||
color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'}
|
||||
className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-3.5!"
|
||||
>
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', { include_count: item.doc_num })}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Space>
|
||||
{/* 全局设置 */}
|
||||
<KnowledgeGlobalConfigModal
|
||||
data={editConfig}
|
||||
|
||||
@@ -83,6 +83,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
>
|
||||
{data && (
|
||||
<div className="rb:mb-6 rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">
|
||||
|
||||
@@ -70,6 +70,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
placeholder={t('knowledgeBase.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
size="middle"
|
||||
/>
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Space, Row, Col } from 'antd';
|
||||
import { Button, Form, Input, Divider } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -16,43 +15,55 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row key={key} gutter={12} className="rb:mb-2">
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block>
|
||||
+ {t('common.add')}
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.jinja-render.mapping')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-24!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
size="small"
|
||||
className="rb:w-39!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
return (
|
||||
<>
|
||||
{values?.memory?.enable && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:text-[12px] rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
|
||||
{t('workflow.config.llm.memory')}
|
||||
<span>{t('workflow.config.llm.inner')}</span>
|
||||
</div>
|
||||
@@ -40,6 +40,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
isArray={false}
|
||||
parentName={[parentName, 'messages']}
|
||||
options={options}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string
|
||||
title?: string;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
isArray?: boolean;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
@@ -15,6 +16,7 @@ interface MessageEditor {
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
@@ -23,11 +25,13 @@ const roleOptions = [
|
||||
]
|
||||
const MessageEditor: FC<MessageEditor> = ({
|
||||
title,
|
||||
titleVariant = 'outlined',
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
size = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -74,14 +78,16 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
|
||||
if (!isArray) {
|
||||
return (
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -90,7 +96,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
@@ -99,16 +105,17 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item {...restField} name={[name, 'role']} noStyle>
|
||||
{currentRole === 'SYSTEM' ? (
|
||||
<Input disabled />
|
||||
<Input disabled className="rb:font-medium!" />
|
||||
) : (
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
className="rb:font-medium!"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
@@ -116,20 +123,23 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
{currentRole !== 'SYSTEM' && (
|
||||
<Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/delete_cycle.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
})}
|
||||
<Form.Item noStyle>
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block>
|
||||
+{t('workflow.addMessage')}
|
||||
<Button type="dashed" size="middle" className="rb:text-[12px]!" onClick={() => handleAdd(add)} block>
|
||||
+ {t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
@@ -74,6 +74,7 @@ const ParamEditModal = forwardRef<ParamEditModalRef, ParamEditModalProps>(({
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Space, List } from 'antd'
|
||||
import Empty from '@/components/Empty'
|
||||
import { Button, Space } from 'antd'
|
||||
|
||||
|
||||
import type { ParamItem, ParamEditModalRef } from './types'
|
||||
import ParamEditModal from './ParamEditModal'
|
||||
@@ -41,47 +41,37 @@ const ParamsList: FC<ParamsListProps> = ({
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{label}</div>
|
||||
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAdd}>+</Button>
|
||||
</Space>
|
||||
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium rb:mb-2">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{value?.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={value}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAdd}>+ {t('workflow.config.parameter-extractor.addParams')}</Button>
|
||||
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.desc}</div>
|
||||
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{value?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
>
|
||||
<div>
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)}) {item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-0.5">{item.desc}</div>
|
||||
</div>
|
||||
|
||||
<Space size={8}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<ParamEditModal
|
||||
ref={paramEditModalRef}
|
||||
|
||||
55
web/src/views/Workflow/components/Properties/RbSlider.tsx
Normal file
55
web/src/views/Workflow/components/Properties/RbSlider.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { InputNumberProps, SliderSingleProps } from 'antd';
|
||||
import { Col, InputNumber, Row, Slider } from 'antd';
|
||||
|
||||
const RbSlider: React.FC<SliderSingleProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step = 0.01,
|
||||
...props
|
||||
}) => {
|
||||
const [curValue, setCurValue] = useState<number | undefined>(0)
|
||||
useEffect(() => {
|
||||
setCurValue(value)
|
||||
}, [value])
|
||||
const handleSliderChange = (newValue: number) => {
|
||||
onChange && onChange(newValue);
|
||||
};
|
||||
|
||||
const handleInputChange: InputNumberProps['onChange'] = (newValue) => {
|
||||
onChange && onChange(newValue as number);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={12}>
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
{...props}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
className="rb:my-0! rb:ml-2.5!"
|
||||
classNames={{
|
||||
rail: 'rb:h-[6px]!',
|
||||
track: 'rb:h-[6px]!'
|
||||
}}
|
||||
onChange={handleSliderChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
onChange={handleInputChange}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
export default RbSlider;
|
||||
@@ -171,6 +171,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
label={t('workflow.config.tool.tool_id')}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
@@ -187,28 +188,30 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
label={parameter.name}
|
||||
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||
rules={[
|
||||
{ required: parameter.required, message: t('workflow.config.tool.required') }
|
||||
{ required: parameter.required, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
|
||||
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
|
||||
>
|
||||
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch />
|
||||
? <Switch size="small" />
|
||||
: parameter.type === 'integer' || parameter.type === 'number'
|
||||
? <InputNumber
|
||||
min={parameter.minimum}
|
||||
max={parameter.maximum}
|
||||
step={parameter.type === 'integer' ? 1 : 0.01}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
className="rb:w-full!"
|
||||
size="small"
|
||||
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
|
||||
/>
|
||||
: <Editor
|
||||
height={32}
|
||||
variant="outlined"
|
||||
options={options}
|
||||
size="small"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { StartVariableItem, VariableEditModalRef } from '../../types'
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SortableList from '@/components/SortableList'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refresh: (values: StartVariableItem) => void;
|
||||
refresh: (values: Variable) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
@@ -36,9 +36,9 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<StartVariableItem>();
|
||||
const [form] = Form.useForm<Variable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<StartVariableItem | null>(null)
|
||||
const [editVo, setEditVo] = useState<Variable | null>(null)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -50,7 +50,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: StartVariableItem) => {
|
||||
const handleOpen = (variable?: Variable) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
setEditVo(variable || null)
|
||||
@@ -85,7 +85,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.start.addVariable')}
|
||||
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.addVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
@@ -96,6 +96,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={initialValues}
|
||||
size="middle"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* 变量类型 */}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Node } from '@antv/x6';
|
||||
import { Space, Button, Divider, App } from 'antd'
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
import type { NodeConfig } from '../../../types'
|
||||
import VariableEditModal from './VariableEditModal'
|
||||
|
||||
interface VariableListProps {
|
||||
selectedNode?: Node | null;
|
||||
config: NodeConfig;
|
||||
value?: Variable[];
|
||||
parentName: string;
|
||||
onChange?: (value: Variable[]) => void;
|
||||
}
|
||||
const VariableList: FC<VariableListProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
selectedNode,
|
||||
config,
|
||||
parentName
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
|
||||
const handleAddVariable = () => {
|
||||
setEditIndex(null)
|
||||
variableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEditVariable = (index: number, vo: Variable) => {
|
||||
variableModalRef.current?.handleOpen(vo)
|
||||
setEditIndex(index)
|
||||
}
|
||||
const handleRefreshVariable = (variable: Variable) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
if (editIndex !== null) {
|
||||
const list = [...value]
|
||||
list[editIndex] = variable
|
||||
onChange?.(list)
|
||||
} else {
|
||||
console.log('VariableList', value, variable)
|
||||
onChange?.([...value, variable])
|
||||
}
|
||||
}
|
||||
const handleDeleteVariable = (index: number, vo: Variable, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!selectedNode) return
|
||||
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const list = [...value]
|
||||
list.splice(index, 1)
|
||||
onChange?.([...list])
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium">
|
||||
{t(`workflow.config.${selectedNode?.data?.type}.${parentName}`)}
|
||||
</div>
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAddVariable}>+ {t('workflow.config.addVariable')}</Button>
|
||||
{Array.isArray(value) && value?.map((vo, index) =>
|
||||
<div
|
||||
key={`${vo.name}}-${index}`}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
>
|
||||
<span className="rb:font-medium">{vo.name}·{vo.description}</span>
|
||||
|
||||
<Space size={8}>
|
||||
{vo.required && <span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{t('workflow.config.start.required')}</span>}
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
<div
|
||||
className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={(e) => handleDeleteVariable(index, vo, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Space>
|
||||
<Divider size="small" />
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<span className="rb:font-medium">sys.{vo.name}</span>
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default VariableList
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface Variable {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
max_length?: number;
|
||||
default?: string;
|
||||
readonly?: boolean;
|
||||
defaultValue?: any;
|
||||
value?: any;
|
||||
}
|
||||
export interface VariableEditModalRef {
|
||||
handleOpen: (values?: Variable) => void;
|
||||
}
|
||||
|
||||
export interface ApiExtensionModalData {
|
||||
name: string;
|
||||
apiEndpoint: string;
|
||||
apiKey: string;
|
||||
}
|
||||
export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ interface VariableSelectProps extends SelectProps {
|
||||
onChange?: (value: string) => void;
|
||||
allowClear?: boolean;
|
||||
filterBooleanType?: boolean;
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
}
|
||||
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
@@ -18,7 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
value,
|
||||
allowClear = true,
|
||||
onChange,
|
||||
size,
|
||||
size = 'middle',
|
||||
filterBooleanType = false,
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { type FC, useEffect, useState, useRef, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd'
|
||||
import { Form, Input, Select, InputNumber, Switch } from 'antd'
|
||||
|
||||
import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef, ChatVariable } from '../../types'
|
||||
import type { NodeConfig, NodeProperties, ChatVariable } from '../../types'
|
||||
import Empty from '@/components/Empty';
|
||||
import emptyIcon from '@/assets/images/workflow/empty.png'
|
||||
import CustomSelect from "@/components/CustomSelect";
|
||||
import VariableEditModal from './VariableEditModal';
|
||||
import MessageEditor from './MessageEditor'
|
||||
import Knowledge from './Knowledge/Knowledge';
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
@@ -23,7 +23,11 @@ import CycleVarsList from './CycleVarsList'
|
||||
import AssignmentList from './AssignmentList'
|
||||
import ToolConfig from './ToolConfig'
|
||||
import MemoryConfig from './MemoryConfig'
|
||||
import VariableList from './VariableList'
|
||||
// import { calculateVariableList } from './utils/variableListCalculator'
|
||||
import styles from './properties.module.css'
|
||||
import Editor from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -42,12 +46,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
chatVariables
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
const [form] = Form.useForm<NodeConfig>();
|
||||
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
|
||||
const values = Form.useWatch([], form);
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
|
||||
const prevMappingNamesRef = useRef<string[]>([])
|
||||
const prevTemplateVarsRef = useRef<string[]>([])
|
||||
@@ -243,49 +244,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
}, [values, selectedNode, form])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
setEditIndex(null)
|
||||
variableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEditVariable = (index: number, vo: StartVariableItem) => {
|
||||
variableModalRef.current?.handleOpen(vo)
|
||||
setEditIndex(index)
|
||||
}
|
||||
const handleRefreshVariable = (value: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
if (editIndex !== null) {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue[editIndex] = value
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
} else {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue, value]
|
||||
}
|
||||
selectedNode?.setData({ ...selectedNode.data})
|
||||
|
||||
setConfigs({ ...selectedNode.data.config })
|
||||
}
|
||||
const handleDeleteVariable = (index: number, vo: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue.splice(index, 1)
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
|
||||
selectedNode?.setData({ ...selectedNode.data })
|
||||
|
||||
setConfigs({ ...selectedNode.data.config })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const variableList = useMemo(() => {
|
||||
if (!selectedNode || !graphRef?.current) return [];
|
||||
|
||||
@@ -586,7 +544,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
break
|
||||
case 'question-classifier':
|
||||
const classNameKey = `${dataNodeId}_class_name`;
|
||||
const outputKey = `${dataNodeId}_output`;
|
||||
// const outputKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(classNameKey)) {
|
||||
addedKeys.add(classNameKey);
|
||||
variableList.push({
|
||||
@@ -1039,11 +997,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
console.log('variableList', variableList)
|
||||
|
||||
return (
|
||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-3">{t('workflow.nodeProperties')}</div>
|
||||
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:pb-3 rb:mb-3 rb:border-b rb:border-b-[#DFE4ED]">{t('workflow.nodeProperties')}</div>
|
||||
{!selectedNode
|
||||
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
|
||||
: <Form form={form} layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-y-auto">
|
||||
: <Form form={form} size="small" layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
|
||||
<Form.Item name="name" label={t('workflow.nodeName')}>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -1073,46 +1031,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
|
||||
<div className="rb:leading-5">
|
||||
{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button>
|
||||
</div>
|
||||
|
||||
<Space size={4} direction="vertical" className="rb:w-full">
|
||||
{Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) =>
|
||||
<div key={`${vo.name}}-${index}`} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md rb:group rb:cursor-pointer">
|
||||
<span>{vo.name}·{vo.description}</span>
|
||||
|
||||
<div className="rb:group-hover:hidden rb:flex rb:items-center rb:gap-1">
|
||||
{vo.required && <span>{t('workflow.config.start.required')}</span>}
|
||||
{vo.type}
|
||||
</div>
|
||||
<Space className="rb:hidden! rb:group-hover:flex!">
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteVariable(index, vo)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<Divider size="small" />
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div>
|
||||
<span>sys.{vo.name}</span>
|
||||
</div>
|
||||
{vo.type}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Form.Item key={key} name={key}>
|
||||
<VariableList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
config={config}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1143,23 +1068,12 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key={key}
|
||||
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
parentName={key}
|
||||
placeholder={t(config.placeholder || 'common.pleaseSelect')}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor
|
||||
key={key}
|
||||
isArray={false}
|
||||
parentName={key}
|
||||
options={variableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
if (config.type === 'define') {
|
||||
return null
|
||||
}
|
||||
@@ -1184,6 +1098,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
titleVariant={config.titleVariant}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -1206,9 +1122,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
name={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
@@ -1226,9 +1142,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (config.type === 'mappingList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
>
|
||||
<Form.Item key={key} name={key} noStyle>
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1238,6 +1152,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
size="small"
|
||||
parentName={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
@@ -1276,87 +1191,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'textarea'
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <Slider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
hasAll={false}
|
||||
valueKey={config.valueKey}
|
||||
labelKey={config.labelKey}
|
||||
/>
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch || variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
if (config.filterChildNodes && selectedNode) {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return [];
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find child nodes whose cycle field equals parent node's ID
|
||||
const childNodes = nodes.filter(node => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
if (config.type === 'conditionList') {
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
noStyle
|
||||
>
|
||||
<ConditionList
|
||||
parentName={key}
|
||||
options={(() => {
|
||||
const cycleVars = values?.cycle_vars || [];
|
||||
@@ -1375,6 +1217,93 @@ const Properties: FC<PropertiesProps> = ({
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={key === 'parallel_count' ? <span className="rb:text-[10px] rb:text-[#5B6167] rb:leading-3.5 rb:-mb-1!">{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}</span> : t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'textarea'
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <RbSlider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
hasAll={false}
|
||||
valueKey={config.valueKey}
|
||||
labelKey={config.labelKey}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t(config.placeholder || 'common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch || variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
if (config.filterChildNodes && selectedNode) {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return [];
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find child nodes whose cycle field equals parent node's ID
|
||||
const childNodes = nodes.filter(node => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'editor'
|
||||
? <Editor options={variableList} variant="outlined" size="small" />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
@@ -1383,11 +1312,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
.properties :global(.ant-input-outlined),
|
||||
.properties :global(.ant-input-number-outlined),
|
||||
.properties :global(.ant-select-outlined:not(.ant-select-customize-input) .ant-select-selector),
|
||||
.properties :global(.ant-input-number-outlined),
|
||||
.properties :global(.ant-input-number-outlined .ant-input-number-handler-wrap) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.properties :global(.ant-input-outlined.ant-input-disabled),
|
||||
.properties :global(.ant-input-outlined[disabled]) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm){
|
||||
height: 28px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table),
|
||||
.properties :global(.ant-table-container),
|
||||
.properties :global(.ant-table-wrapper table) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:first-child) {
|
||||
border-start-start-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:last-child) {
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:first-child) {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:last-child) {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container) {
|
||||
border-start-start-radius: 6px;
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-container) {
|
||||
/* border-left: none;
|
||||
border-top: none;
|
||||
border-bottom: none; */
|
||||
border: none;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-form-item-horizontal.ant-form-item .ant-form-item-control-input-content:has(> .ant-switch:only-child, > .ant-rate:only-child)) {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.properties :global(.ant-divider-horizontal.ant-divider-sm) {
|
||||
margin-block: 16px;
|
||||
}
|
||||
.properties :global(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.properties :global(.ant-form-item .ant-form-item-label>label) {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm .ant-select-selector),
|
||||
.properties :global(.ant-select-dropdown .ant-select-item),.properties :global(.ant-input-number-sm) {
|
||||
font-size: 12px;
|
||||
}
|
||||
.properties :global(.ant-input-number-out-of-range .ant-input-number-input-wrap input) {
|
||||
color: #212332;
|
||||
}
|
||||
.properties :global(.ant-slider-horizontal .ant-slider-step) {
|
||||
height: 6px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) {
|
||||
padding: 0 4px 0 6px ;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-item),
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-placeholder) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.properties :global(.ant-select .ant-select-arrow) {
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: "end", icon: endIcon,
|
||||
config: {
|
||||
output: {
|
||||
type: 'define'
|
||||
type: 'editor'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,6 +125,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
context: {
|
||||
type: 'variableList',
|
||||
placeholder: 'workflow.config.llm.contextPlaceholder'
|
||||
},
|
||||
messages: {
|
||||
type: 'define',
|
||||
@@ -134,7 +135,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
content: undefined,
|
||||
readonly: true
|
||||
},
|
||||
]
|
||||
],
|
||||
placeholder: 'workflow.config.llm.messagesPlaceholder'
|
||||
},
|
||||
memory: {
|
||||
type: 'memoryConfig',
|
||||
@@ -170,7 +172,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
text: {
|
||||
type: 'variableList',
|
||||
filterLoopIterationVars: true
|
||||
filterLoopIterationVars: true,
|
||||
placeholder: 'workflow.config.parameter-extractor.textPlaceholder'
|
||||
},
|
||||
params: {
|
||||
type: 'paramList',
|
||||
@@ -178,6 +181,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
titleVariant: 'borderless',
|
||||
placeholder: 'workflow.config.parameter-extractor.promptPlaceholder'
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -189,7 +194,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "memory-read", icon: memoryReadIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
type: 'editor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
@@ -212,7 +217,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "memory-write", icon: memoryWriteIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
type: 'editor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
@@ -270,7 +275,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
user_supplement_prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false
|
||||
isArray: false,
|
||||
titleVariant: 'borderless'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -436,6 +442,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
titleVariant: 'borderless',
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user