Merge branch 'refs/heads/develop' into fix/memory_bug_fix

This commit is contained in:
lixinyue
2026-01-19 18:04:29 +08:00
102 changed files with 3013 additions and 1540 deletions

11
api/app/cache/__init__.py vendored Normal file
View File

@@ -0,0 +1,11 @@
"""
Cache 缓存模块
提供各种缓存功能的统一入口
"""
from .memory import EmotionMemoryCache, ImplicitMemoryCache
__all__ = [
"EmotionMemoryCache",
"ImplicitMemoryCache",
]

12
api/app/cache/memory/__init__.py vendored Normal file
View 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
View 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: 用户IDend_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: 用户IDend_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: 用户IDend_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: 用户IDend_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
View 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: 用户IDend_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: 用户IDend_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: 用户IDend_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: 用户IDend_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

View File

@@ -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)}"
)

View File

@@ -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="系统版本获取成功"
)

View File

@@ -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)

View File

@@ -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(
"工作流试运行返回结果",

View File

@@ -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(
"工作流试运行返回结果",

View File

@@ -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")

View File

@@ -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,
},
],
}

View File

@@ -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,
},
],
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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
}

View File

@@ -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}

View File

@@ -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
)

View File

@@ -66,7 +66,7 @@ class LLMNodeConfig(BaseNodeConfig):
)
memory: MemoryWindowSetting = Field(
...,
default_factory=MemoryWindowSetting,
description="对话上下文窗口"
)

View File

@@ -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}

View File

@@ -76,5 +76,5 @@ __all__ = [
"ToolStatus",
"AuthType",
"ExecutionStatus",
"MemoryPerceptualModel"
"MemoryPerceptualModel",
]

View File

@@ -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"),

View File

@@ -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
"""

View File

@@ -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)}")

View File

@@ -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,

View File

@@ -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模型")

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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)
# 不抛出异常,缓存失败不应影响主流程

View File

@@ -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

View File

@@ -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)
# 不抛出异常,缓存失败不应影响主流程

View File

@@ -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(

View File

@@ -53,7 +53,7 @@ nodes:
type: end
name: 结束
config:
output: "{{ llm_qa.output }}"
output: "{{llm_qa.output}}"
position:
x: 900
y: 100

View File

@@ -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", []),

View File

@@ -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 systems 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!"
]
}
}
}
}

View 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 ###

View 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 ###

View File

@@ -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`

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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>
</>),
},
{

View File

@@ -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()
}
}, []);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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}

View File

@@ -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"
/>

View File

@@ -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',

View File

@@ -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: '清空',

View File

@@ -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
}
}
};

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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' ? (

View File

@@ -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')
) : (

View File

@@ -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}

View File

@@ -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' &&
<>

View File

@@ -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>
);
});

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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])

View File

@@ -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',
}}
>

View File

@@ -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]);

View File

@@ -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>

View File

@@ -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>
</>
)
}

View File

@@ -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>

View File

@@ -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>
</>
)

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />}

View File

@@ -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')}

View File

@@ -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}

View File

@@ -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]">

View File

@@ -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>

View File

@@ -117,6 +117,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
placeholder={t('knowledgeBase.searchPlaceholder')}
onSearch={handleSearch}
style={{ width: '100%' }}
size="middle"
/>
{filterList.length === 0
? <Empty />

View File

@@ -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 />
</>
)
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -74,6 +74,7 @@ const ParamEditModal = forwardRef<ParamEditModalRef, ParamEditModalProps>(({
<Form
form={form}
layout="vertical"
size="middle"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
>
<FormItem

View File

@@ -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}

View 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;

View File

@@ -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')}
/>
}

View File

@@ -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 }}
>
{/* 变量类型 */}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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
}) => {

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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